web-corders-vrt 0.1.5 → 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,24 +2,30 @@
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
16
  初回のみ Playwright のブラウザが必要:
11
17
 
12
18
  ```bash
13
- npx playwright install chromium
19
+ npx playwright install chromium #通常はインストール済みのChromeが使われるはず
14
20
  ```
15
21
 
16
- `init` コマンドで、Claude Codeのスキルファイル (`.claude/skills/web-corders-vrt/SKILL.md`) を自動生成できる。
22
+ `init` コマンドで、Claude Codeのスキルファイル (`.claude/skills/web-corders-vrt/SKILL.md`) を自動生成する
17
23
 
18
24
  ```bash
19
25
  npx web-corders-vrt init --refDomain https://example.com
20
26
  ```
21
27
 
22
- ## 基本的な使い方
28
+ ## 手動での使い方
23
29
 
24
30
  ```bash
25
31
  npx web-corders-vrt run \
@@ -83,8 +89,6 @@ vrt-results/2026-02-28T10-00-00/
83
89
  | `--paths <paths>` | (必須) | テスト対象のページパス(カンマ区切り) |
84
90
  | `--threshold <n>` | `0.1` | 差分許容率(%)。これ以下の差分はPASS扱い |
85
91
  | `--hide <selectors>` | — | 非表示にするCSSセレクタ(カンマ区切り)。例: `.ad-banner,.cookie-popup` |
86
- | `--no-html` | — | HTMLレポートを生成しない |
87
- | `--no-open` | — | HTMLレポートをブラウザで自動的に開かない |
88
92
 
89
93
  ビューポートはSP(375x812)とPC(1440x900)の2種類で固定。フルページスクリーンショットを取得し、結果は `./vrt-results/` に出力される。
90
94
 
package/bin/vrt.ts CHANGED
@@ -19,8 +19,6 @@ program
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);
@@ -31,8 +29,6 @@ program
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);
package/dist/bin/vrt.js CHANGED
@@ -10,9 +10,7 @@ var cliOptionsSchema = z.object({
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
@@ -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 {
@@ -554,12 +548,12 @@ 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";
560
554
 
561
555
  // src/templates/SKILL-TEMPLATE.md
562
- 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\u3059\u308B\n7. \u30BD\u30FC\u30B9\u30B3\u30FC\u30C9\u3092\u4FEE\u6B63\u3059\u308B\n8. \u518D\u5EA6VRT\u3092\u5B9F\u884C\u3057\u3066\u4FEE\u6B63\u304C\u53CD\u6620\u3055\u308C\u305F\u3053\u3068\u3092\u78BA\u8A8D\u3059\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';
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';
563
557
 
564
558
  // src/commands/init.ts
565
559
  async function runInit(options) {
@@ -577,19 +571,37 @@ async function runInit(options) {
577
571
  );
578
572
  await writeFile4(outFile, content, "utf-8");
579
573
  console.log(chalk3.green(`\u2705 Created ${outFile}`));
574
+ await ensureGitignore(process.cwd());
580
575
  console.log("");
581
576
  console.log(
582
- "This skill file allows Claude Code to run VRT with the /vrt command."
577
+ "Review and customize the skill file, then commit it to your repository."
583
578
  );
584
- console.log(
585
- "Review and customize the file, then commit it to your repository."
579
+ }
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
+ }
588
+ const lines = content.split("\n");
589
+ if (lines.some((line) => line.trim() === VRT_RESULTS_PATTERN)) {
590
+ return;
591
+ }
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"
586
597
  );
598
+ console.log(chalk3.green(`\u2705 Added '${VRT_RESULTS_PATTERN}' to .gitignore`));
587
599
  }
588
600
 
589
601
  // bin/vrt.ts
590
602
  var program = new Command();
591
603
  program.name("vrt").description("Visual Regression Testing CLI - Compare web pages visually").version("0.1.0");
592
- 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)").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) => {
593
605
  try {
594
606
  const parsed = cliOptionsSchema.parse(rawOptions);
595
607
  const options = {
@@ -597,9 +609,7 @@ program.command("run").description("Run visual regression tests").requiredOption
597
609
  afterUrl: parsed.after,
598
610
  paths: parsed.paths,
599
611
  threshold: parsed.threshold,
600
- hideSelectors: parsed.hide,
601
- html: parsed.html,
602
- open: parsed.open
612
+ hideSelectors: parsed.hide
603
613
  };
604
614
  const report = await runVrt(options);
605
615
  process.exit(report.summary.overallStatus === "pass" ? 0 : 1);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/vrt.ts","../../src/schemas.ts","../../src/commands/run.ts","../../src/core/screenshotter.ts","../../src/constants.ts","../../src/core/stabilizer.ts","../../src/core/comparator.ts","../../src/reporters/terminal.ts","../../src/reporters/json.ts","../../src/reporters/html.ts","../../src/templates/report.html","../../src/templates/report.css","../../src/commands/init.ts","../../src/templates/SKILL-TEMPLATE.md"],"sourcesContent":["import { Command } from \"commander\";\nimport { cliOptionsSchema } from \"../src/schemas.js\";\nimport { runVrt } from \"../src/commands/run.js\";\nimport { runInit } from \"../src/commands/init.js\";\nimport type { ResolvedOptions } from \"../src/types.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"vrt\")\n .description(\"Visual Regression Testing CLI - Compare web pages visually\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"run\")\n .description(\"Run visual regression tests\")\n .requiredOption(\"--reference <url>\", \"Reference URL (production)\")\n .requiredOption(\"--after <url>\", \"Comparison URL (local/staging)\")\n .requiredOption(\"--paths <paths>\", \"Page paths to compare (comma-separated)\")\n .option(\"--threshold <n>\", \"Diff tolerance percentage\", \"0.1\")\n .option(\"--hide <selectors>\", \"CSS selectors to hide (comma-separated)\")\n .option(\"--no-html\", \"Skip HTML report generation\")\n .option(\"--no-open\", \"Do not open HTML report in browser\")\n .action(async (rawOptions) => {\n try {\n const parsed = cliOptionsSchema.parse(rawOptions);\n\n const options: ResolvedOptions = {\n referenceUrl: parsed.reference,\n afterUrl: parsed.after,\n paths: parsed.paths,\n threshold: parsed.threshold,\n hideSelectors: parsed.hide,\n html: parsed.html,\n open: parsed.open,\n };\n\n const report = await runVrt(options);\n process.exit(report.summary.overallStatus === \"pass\" ? 0 : 1);\n } catch (error) {\n if (error instanceof Error) {\n console.error(`Error: ${error.message}`);\n } else {\n console.error(\"An unexpected error occurred\");\n }\n process.exit(2);\n }\n });\n\nprogram\n .command(\"init\")\n .description(\n \"Generate .claude/skills/web-corders-vrt/SKILL.md for this repository\",\n )\n .requiredOption(\"--refDomain <domain>\", \"Reference domain URL (production)\")\n .action(async (options) => {\n try {\n await runInit({ refDomain: options.refDomain });\n } catch (error) {\n if (error instanceof Error) {\n console.error(`Error: ${error.message}`);\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import { z } from \"zod\";\n\nexport const cliOptionsSchema = z.object({\n reference: z.string().url(\"--reference must be a valid URL\"),\n after: z.string().url(\"--after must be a valid URL\"),\n paths: z.string().transform((val) => val.split(\",\").map((p) => p.trim())),\n threshold: z.coerce.number().min(0).max(100).default(0.1),\n hide: z\n .string()\n .optional()\n .transform((val) => (val ? val.split(\",\").map((s) => s.trim()) : [])),\n html: z.boolean().default(true),\n open: z.boolean().default(true),\n});\n\nexport type CliOptions = z.input<typeof cliOptionsSchema>;\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport { Screenshotter } from \"../core/screenshotter.js\";\nimport { compareImages } from \"../core/comparator.js\";\nimport { printTerminalReport } from \"../reporters/terminal.js\";\nimport { writeJsonReport } from \"../reporters/json.js\";\nimport { writeHtmlReport } from \"../reporters/html.js\";\nimport {\n DEFAULT_SP_VIEWPORT,\n DEFAULT_PC_VIEWPORT,\n DEFAULT_OUT_DIR,\n DEFAULT_THRESHOLD,\n} from \"../constants.js\";\nimport type {\n ResolvedOptions,\n VrtReport,\n VrtTestResult,\n ViewportType,\n} from \"../types.js\";\n\nconst VIEWPORTS: ViewportType[] = [\"sp\", \"pc\"];\n\n/**\n * VRTのメイン実行コマンド。\n * スクリーンショット取得 → 比較 → レポート生成を一気に行う。\n */\nexport async function runVrt(options: ResolvedOptions): Promise<VrtReport> {\n const startTime = Date.now();\n\n // 出力ディレクトリを作成\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\n const runDir = join(DEFAULT_OUT_DIR, timestamp);\n const screenshotsDir = join(runDir, \"screenshots\");\n await mkdir(screenshotsDir, { recursive: true });\n\n const screenshotter = new Screenshotter();\n const spinner = ora(\"Initializing browser...\").start();\n\n try {\n await screenshotter.initialize();\n\n // Reference スクリーンショット取得\n spinner.text = `Taking screenshots of ${chalk.cyan(options.referenceUrl)}...`;\n const referenceScreenshots = await screenshotter.captureAll(\n options.referenceUrl,\n options.paths,\n VIEWPORTS,\n options.hideSelectors,\n );\n\n // After スクリーンショット取得\n spinner.text = `Taking screenshots of ${chalk.cyan(options.afterUrl)}...`;\n const afterScreenshots = await screenshotter.captureAll(\n options.afterUrl,\n options.paths,\n VIEWPORTS,\n options.hideSelectors,\n );\n\n spinner.text = \"Comparing screenshots...\";\n\n // 比較を実行\n const results: VrtTestResult[] = [];\n\n for (const referenceShot of referenceScreenshots) {\n const afterShot = afterScreenshots.find(\n (a) =>\n a.pagePath === referenceShot.pagePath &&\n a.viewportType === referenceShot.viewportType,\n );\n\n if (!afterShot) {\n results.push(\n createErrorResult(\n referenceShot.pagePath,\n referenceShot.viewportType,\n options,\n \"After screenshot not found\",\n ),\n );\n continue;\n }\n\n try {\n const comparison = compareImages(\n referenceShot.buffer,\n afterShot.buffer,\n options.threshold,\n );\n // スクリーンショットを保存\n const filePrefix = `${referenceShot.pageName}--${referenceShot.viewportType}`;\n const referencePath = join(\"screenshots\", `${filePrefix}--reference.png`);\n const afterPath = join(\"screenshots\", `${filePrefix}--after.png`);\n const diffPath = join(\"screenshots\", `${filePrefix}--diff.png`);\n\n await writeFile(join(runDir, referencePath), referenceShot.buffer);\n await writeFile(join(runDir, afterPath), afterShot.buffer);\n await writeFile(join(runDir, diffPath), comparison.diffImage);\n\n const pageName =\n referenceShot.pagePath === \"/\"\n ? \"トップページ\"\n : referenceShot.pagePath.replace(/^\\//, \"\");\n const viewport = getViewportDimensions(referenceShot.viewportType);\n\n results.push({\n page: {\n path: referenceShot.pagePath,\n name: pageName,\n url: {\n reference: new URL(\n referenceShot.pagePath,\n options.referenceUrl,\n ).toString(),\n after: new URL(referenceShot.pagePath, options.afterUrl).toString(),\n },\n },\n viewport: {\n type: referenceShot.viewportType,\n ...viewport,\n },\n status: comparison.passed ? \"pass\" : \"fail\",\n comparison: {\n diffPercentage: comparison.diffPercentage,\n diffPixelCount: comparison.diffCount,\n totalPixels: comparison.totalPixels,\n threshold: options.threshold,\n dimensions: comparison.dimensions,\n },\n screenshots: {\n reference: referencePath,\n after: afterPath,\n diff: diffPath,\n },\n });\n } catch (error) {\n results.push(\n createErrorResult(\n referenceShot.pagePath,\n referenceShot.viewportType,\n options,\n error instanceof Error ? error.message : String(error),\n ),\n );\n }\n }\n\n const duration = Date.now() - startTime;\n const passed = results.filter((r) => r.status === \"pass\").length;\n const failed = results.filter((r) => r.status === \"fail\").length;\n const errored = results.filter((r) => r.status === \"error\").length;\n\n const report: VrtReport = {\n version: \"1.0\",\n meta: {\n timestamp,\n referenceUrl: options.referenceUrl,\n afterUrl: options.afterUrl,\n duration,\n command: buildCommandString(options),\n },\n summary: {\n totalTests: results.length,\n passed,\n failed,\n errored,\n overallStatus: failed > 0 || errored > 0 ? \"fail\" : \"pass\",\n },\n results,\n };\n\n spinner.stop();\n\n // レポート出力\n printTerminalReport(report);\n\n const jsonPath = await writeJsonReport(report, runDir);\n console.log(`JSON report: ${chalk.dim(jsonPath)}`);\n\n if (options.html) {\n const htmlPath = await writeHtmlReport(report, runDir);\n console.log(`HTML report: ${chalk.dim(htmlPath)}`);\n\n if (options.open) {\n const { exec } = await import(\"node:child_process\");\n exec(`open \"${htmlPath}\"`);\n }\n }\n\n console.log(`Output dir: ${chalk.dim(runDir)}`);\n\n return report;\n } finally {\n await screenshotter.cleanup();\n }\n}\n\nfunction getViewportDimensions(\n type: ViewportType,\n): { width: number; height: number } {\n return type === \"sp\"\n ? { ...DEFAULT_SP_VIEWPORT }\n : { ...DEFAULT_PC_VIEWPORT };\n}\n\nfunction createErrorResult(\n pagePath: string,\n viewportType: ViewportType,\n options: ResolvedOptions,\n error: string,\n): VrtTestResult {\n const viewport = getViewportDimensions(viewportType);\n const pageName =\n pagePath === \"/\" ? \"トップページ\" : pagePath.replace(/^\\//, \"\");\n return {\n page: {\n path: pagePath,\n name: pageName,\n url: {\n reference: new URL(pagePath, options.referenceUrl).toString(),\n after: new URL(pagePath, options.afterUrl).toString(),\n },\n },\n viewport: { type: viewportType, ...viewport },\n status: \"error\",\n comparison: {\n diffPercentage: 0,\n diffPixelCount: 0,\n totalPixels: 0,\n threshold: options.threshold,\n dimensions: { width: 0, height: 0, referenceHeight: 0, afterHeight: 0 },\n },\n screenshots: { reference: \"\", after: \"\", diff: \"\" },\n error,\n };\n}\n\nfunction buildCommandString(options: ResolvedOptions): string {\n const parts = [\"npx web-corders-vrt run\"];\n parts.push(`--reference ${options.referenceUrl}`);\n parts.push(`--after ${options.afterUrl}`);\n parts.push(`--paths ${options.paths.join(\",\")}`);\n if (options.threshold !== DEFAULT_THRESHOLD) {\n parts.push(`--threshold ${options.threshold}`);\n }\n if (options.hideSelectors.length > 0) {\n parts.push(`--hide \"${options.hideSelectors.join(\",\")}\"`);\n }\n return parts.join(\" \\\\\\n \");\n}\n","import { chromium, type Browser, type BrowserContext } from \"playwright\";\nimport type { ScreenshotResult, ViewportType } from \"../types.js\";\nimport {\n DEFAULT_SP_VIEWPORT,\n DEFAULT_PC_VIEWPORT,\n DEFAULT_DELAY,\n DEFAULT_CONCURRENCY,\n SP_USER_AGENT,\n BLOCKED_DOMAINS,\n} from \"../constants.js\";\nimport { stabilizePage, getDateMockScript } from \"./stabilizer.js\";\n\nexport class Screenshotter {\n private browser: Browser | null = null;\n\n async initialize(): Promise<void> {\n this.browser = await chromium.launch();\n }\n\n /**\n * 指定URLのスクリーンショットを取得する。\n */\n async capture(\n url: string,\n pagePath: string,\n viewportType: ViewportType,\n hideSelectors: string[],\n ): Promise<ScreenshotResult> {\n if (!this.browser) {\n throw new Error(\"Browser not initialized. Call initialize() first.\");\n }\n\n const viewport =\n viewportType === \"sp\"\n ? { ...DEFAULT_SP_VIEWPORT }\n : { ...DEFAULT_PC_VIEWPORT };\n\n const context: BrowserContext = await this.browser.newContext({\n viewport,\n deviceScaleFactor: viewportType === \"sp\" ? 2 : 1,\n userAgent: viewportType === \"sp\" ? SP_USER_AGENT : undefined,\n });\n\n const page = await context.newPage();\n\n // 日付をモック\n await page.addInitScript(getDateMockScript());\n\n // 広告・計測系リクエストをブロック\n await page.route(\"**/*\", (route) => {\n const reqUrl = route.request().url();\n const shouldBlock = BLOCKED_DOMAINS.some((domain) =>\n reqUrl.includes(domain),\n );\n if (shouldBlock) {\n return route.abort();\n }\n return route.continue();\n });\n\n // ページにアクセス\n await page.goto(url, { waitUntil: \"load\", timeout: 60000 });\n\n // ページの安定化\n await stabilizePage(page, {\n hideSelectors,\n delay: DEFAULT_DELAY,\n });\n\n // スクリーンショット取得\n const buffer = await page.screenshot({\n fullPage: true,\n type: \"png\",\n });\n\n // 画像サイズを取得(PNGヘッダから読む)\n const width = buffer.readUInt32BE(16);\n const height = buffer.readUInt32BE(20);\n\n await context.close();\n\n // パスからページ名を生成\n const pageName =\n pagePath === \"/\"\n ? \"top\"\n : pagePath.replace(/^\\//, \"\").replace(/\\//g, \"-\");\n\n return {\n pagePath,\n pageName,\n viewportType,\n buffer: Buffer.from(buffer),\n width,\n height,\n };\n }\n\n /**\n * 複数のページ・ビューポートのスクリーンショットを並行で取得する。\n */\n async captureAll(\n baseUrl: string,\n paths: string[],\n viewports: ViewportType[],\n hideSelectors: string[],\n ): Promise<ScreenshotResult[]> {\n const tasks: Array<{ url: string; path: string; viewport: ViewportType }> =\n [];\n\n for (const pagePath of paths) {\n const url = new URL(pagePath, baseUrl).toString();\n for (const viewport of viewports) {\n tasks.push({ url, path: pagePath, viewport });\n }\n }\n\n // 並行実行制御\n const results: ScreenshotResult[] = [];\n\n for (let i = 0; i < tasks.length; i += DEFAULT_CONCURRENCY) {\n const batch = tasks.slice(i, i + DEFAULT_CONCURRENCY);\n const batchResults = await Promise.all(\n batch.map((task) =>\n this.capture(task.url, task.path, task.viewport, hideSelectors),\n ),\n );\n results.push(...batchResults);\n }\n\n return results;\n }\n\n async cleanup(): Promise<void> {\n await this.browser?.close();\n this.browser = null;\n }\n}\n","export const DEFAULT_SP_VIEWPORT = { width: 375, height: 812 } as const;\nexport const DEFAULT_PC_VIEWPORT = { width: 1440, height: 900 } as const;\n\nexport const DEFAULT_THRESHOLD = 0.1;\nexport const DEFAULT_DELAY = 1000;\nexport const DEFAULT_CONCURRENCY = 3;\nexport const DEFAULT_OUT_DIR = \"./vrt-results\";\n\nexport const SP_USER_AGENT =\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\";\n\n/** VRTに不要な広告・計測系ドメイン(常にブロック) */\nexport const BLOCKED_DOMAINS = [\n \"googletagmanager.com\",\n \"google-analytics.com\",\n \"googleadservices.com\",\n \"googlesyndication.com\",\n \"doubleclick.net\",\n \"facebook.net\",\n \"fbcdn.net\",\n \"analytics.yahoo.co.jp\",\n \"clarity.ms\",\n \"hotjar.com\",\n \"newrelic.com\",\n \"sentry.io\",\n \"datadoghq.com\",\n];\n\n/** VRTに不要な開発ツール系要素(常に非表示) */\nexport const DEFAULT_HIDE_SELECTORS = [\"#devtools-indicator\"];\n\n/** アニメーション・トランジションを無効化するCSS */\nexport const DISABLE_ANIMATIONS_CSS = `\n*, *::before, *::after {\n animation-duration: 0s !important;\n animation-delay: 0s !important;\n transition-duration: 0s !important;\n transition-delay: 0s !important;\n scroll-behavior: auto !important;\n caret-color: transparent !important;\n}\n`;\n","import type { Page } from \"playwright\";\nimport {\n DISABLE_ANIMATIONS_CSS,\n DEFAULT_HIDE_SELECTORS,\n} from \"../constants.js\";\n\nexport interface StabilizeOptions {\n disableAnimations?: boolean;\n hideSelectors?: string[];\n delay?: number;\n}\n\n/**\n * ページの動的コンテンツを安定化させる。\n * アニメーション無効化、要素の非表示、日付のモックなどを行う。\n */\nexport async function stabilizePage(\n page: Page,\n options: StabilizeOptions,\n): Promise<void> {\n // アニメーション・トランジションを無効化\n if (options.disableAnimations !== false) {\n await page.addStyleTag({ content: DISABLE_ANIMATIONS_CSS });\n }\n\n // 指定要素を非表示にする(visibility: hidden でレイアウトを崩さない)\n const allHideSelectors = [\n ...DEFAULT_HIDE_SELECTORS,\n ...(options.hideSelectors ?? []),\n ];\n if (allHideSelectors.length > 0) {\n const hideCSS = allHideSelectors\n .map((s) => `${s} { visibility: hidden !important; }`)\n .join(\"\\n\");\n await page.addStyleTag({ content: hideCSS });\n }\n\n // 追加の待機時間(hydration完了を待つ)\n if (options.delay && options.delay > 0) {\n await page.waitForTimeout(options.delay);\n }\n\n // Shadow DOM内の要素はCSSが届かないのでJSで削除(Next.js devtools等)\n if (allHideSelectors.length > 0) {\n await page.evaluate(`\n for (const sel of ${JSON.stringify(allHideSelectors)}) {\n document.querySelectorAll(sel).forEach(el => el.remove());\n document.querySelectorAll(\"*\").forEach(el => {\n if (el.shadowRoot) {\n el.shadowRoot.querySelectorAll(sel).forEach(inner => inner.remove());\n }\n });\n }\n `);\n }\n}\n\n/**\n * 日付をモックして固定値にする初期化スクリプト。\n * page.goto() の前に page.addInitScript() で使う。\n */\nexport function getDateMockScript(): string {\n return `\n (() => {\n const fixedDate = new Date('2025-01-01T00:00:00Z');\n const OrigDate = Date;\n const MockDate = class extends OrigDate {\n constructor(...args) {\n if (args.length === 0) {\n super(fixedDate.getTime());\n } else {\n super(...args);\n }\n }\n static now() { return fixedDate.getTime(); }\n };\n globalThis.Date = MockDate;\n })();\n `;\n}\n","import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport type { ComparisonResult } from \"../types.js\";\n\n/**\n * 2つのPNG画像をピクセル単位で比較し、差分画像を生成する。\n * サイズが異なる場合は大きい方に合わせて白パディングで拡張する。\n */\nexport function compareImages(\n referenceBuffer: Buffer,\n afterBuffer: Buffer,\n threshold: number = 0.1,\n): ComparisonResult {\n const reference = PNG.sync.read(referenceBuffer);\n const after = PNG.sync.read(afterBuffer);\n\n const width = Math.max(reference.width, after.width);\n const height = Math.max(reference.height, after.height);\n\n // サイズが異なる場合は正規化\n const normalizedReference = normalizeImage(reference, width, height);\n const normalizedAfter = normalizeImage(after, width, height);\n\n const diff = new PNG({ width, height });\n\n const diffCount = pixelmatch(\n normalizedReference.data,\n normalizedAfter.data,\n diff.data,\n width,\n height,\n {\n threshold: 0.1,\n includeAA: false,\n alpha: 0.1,\n diffColor: [255, 0, 0],\n diffColorAlt: [0, 200, 0],\n },\n );\n\n const totalPixels = width * height;\n const diffPercentage = (diffCount / totalPixels) * 100;\n\n return {\n diffCount,\n totalPixels,\n diffPercentage,\n diffImage: PNG.sync.write(diff),\n passed: diffPercentage <= threshold,\n dimensions: {\n width,\n height,\n referenceHeight: reference.height,\n afterHeight: after.height,\n },\n };\n}\n\n/**\n * 画像を指定サイズに正規化する。\n * 小さい場合は白(#ffffff)でパディングする。\n */\nfunction normalizeImage(\n png: PNG,\n targetWidth: number,\n targetHeight: number,\n): PNG {\n if (png.width === targetWidth && png.height === targetHeight) {\n return png;\n }\n\n const normalized = new PNG({ width: targetWidth, height: targetHeight });\n\n // 白で埋める\n for (let i = 0; i < normalized.data.length; i += 4) {\n normalized.data[i] = 255; // R\n normalized.data[i + 1] = 255; // G\n normalized.data[i + 2] = 255; // B\n normalized.data[i + 3] = 255; // A\n }\n\n // 元の画像をコピー\n for (let y = 0; y < png.height; y++) {\n for (let x = 0; x < png.width; x++) {\n const srcIdx = (y * png.width + x) * 4;\n const dstIdx = (y * targetWidth + x) * 4;\n normalized.data[dstIdx] = png.data[srcIdx];\n normalized.data[dstIdx + 1] = png.data[srcIdx + 1];\n normalized.data[dstIdx + 2] = png.data[srcIdx + 2];\n normalized.data[dstIdx + 3] = png.data[srcIdx + 3];\n }\n }\n\n return normalized;\n}\n","import chalk from \"chalk\";\nimport type { VrtReport } from \"../types.js\";\n\n/**\n * ターミナルにカラー出力でVRT結果を表示する。\n */\nexport function printTerminalReport(report: VrtReport): void {\n const { meta, summary, results } = report;\n\n console.log(\"\");\n console.log(chalk.bold(`VRT Results - ${meta.timestamp}`));\n console.log(\"=\".repeat(50));\n console.log(\"\");\n console.log(\n `Comparing: ${chalk.cyan(meta.referenceUrl)} vs ${chalk.cyan(meta.afterUrl)}`,\n );\n console.log(\"\");\n\n // ページごとにグループ化\n const groupedByPage = new Map<string, typeof results>();\n for (const result of results) {\n const key = result.page.path;\n if (!groupedByPage.has(key)) {\n groupedByPage.set(key, []);\n }\n groupedByPage.get(key)!.push(result);\n }\n\n for (const [pagePath, pageResults] of groupedByPage) {\n const pageName = pageResults[0].page.name;\n console.log(` Page: ${chalk.bold(pageName)} (${pagePath})`);\n\n for (const result of pageResults) {\n const vpLabel = `${result.viewport.type.toUpperCase()} (${result.viewport.width}x${result.viewport.height})`;\n\n if (result.status === \"error\") {\n console.log(\n ` ${vpLabel} ${chalk.yellow(\"⚠ ERROR\")} ${result.error || \"Unknown error\"}`,\n );\n continue;\n }\n\n const diffStr = `diff: ${result.comparison.diffPercentage.toFixed(2)}%`;\n\n if (result.status === \"pass\") {\n console.log(\n ` ${vpLabel} ${chalk.green(\"✅ PASS\")} ${chalk.dim(diffStr)}`,\n );\n } else {\n const thresholdStr = `(threshold: ${result.comparison.threshold}%)`;\n console.log(\n ` ${vpLabel} ${chalk.red(\"❌ FAIL\")} ${chalk.red(diffStr)} ${chalk.dim(thresholdStr)}`,\n );\n\n }\n }\n\n console.log(\"\");\n }\n\n // サマリー\n const statusColor =\n summary.overallStatus === \"pass\" ? chalk.green : chalk.red;\n console.log(\n `Summary: ${chalk.green(`${summary.passed} passed`)}, ${summary.failed > 0 ? chalk.red(`${summary.failed} failed`) : `${summary.failed} failed`}${summary.errored > 0 ? `, ${chalk.yellow(`${summary.errored} errored`)}` : \"\"} / ${summary.totalTests} total`,\n );\n console.log(`Overall: ${statusColor(summary.overallStatus.toUpperCase())}`);\n console.log(`Duration: ${(meta.duration / 1000).toFixed(1)}s`);\n console.log(\"\");\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { VrtReport } from \"../types.js\";\n\n/**\n * VrtReport を JSON ファイルとして保存する。\n */\nexport async function writeJsonReport(\n report: VrtReport,\n outDir: string,\n): Promise<string> {\n const filePath = join(outDir, \"report.json\");\n await writeFile(filePath, JSON.stringify(report, null, 2), \"utf-8\");\n return filePath;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { VrtReport, VrtTestResult } from \"../types.js\";\nimport reportTemplate from \"../templates/report.html\";\nimport reportStyles from \"../templates/report.css\";\n\n/**\n * 単一HTMLファイルのレポートを生成する。\n * 外部依存なし、CSSはインライン。\n */\nexport async function writeHtmlReport(\n report: VrtReport,\n outDir: string,\n): Promise<string> {\n const html = generateHtml(report);\n const filePath = join(outDir, \"report.html\");\n await writeFile(filePath, html, \"utf-8\");\n return filePath;\n}\n\nfunction generateHtml(report: VrtReport): string {\n const { meta, summary, results } = report;\n\n const resultCards = results.map((r) => generateResultCard(r)).join(\"\\n\");\n\n const failedStat =\n summary.failed > 0\n ? `<div class=\"stat fail\">${summary.failed} Failed</div>`\n : \"\";\n\n return (reportTemplate as string)\n .replace(\"{{CSS}}\", reportStyles as string)\n .replace(/\\{\\{TIMESTAMP\\}\\}/g, meta.timestamp)\n .replace(\"{{DURATION}}\", (meta.duration / 1000).toFixed(1))\n .replace(\"{{REFERENCE_URL}}\", meta.referenceUrl)\n .replace(\"{{AFTER_URL}}\", meta.afterUrl)\n .replace(\"{{TOTAL}}\", String(summary.totalTests))\n .replace(\"{{PASSED}}\", String(summary.passed))\n .replace(\"{{FAILED_STAT}}\", failedStat)\n .replace(\"{{RESULTS}}\", resultCards);\n}\n\nfunction generateResultCard(result: VrtTestResult): string {\n const { page, viewport, status, comparison, screenshots } = result;\n const vpLabel = viewport.type.toUpperCase();\n\n const imagesHtml = `\n <div class=\"comparison\">\n <div class=\"images\">\n <div class=\"img-container img-reference\">\n <div class=\"label\">Reference</div>\n <img src=\"${screenshots.reference}\" alt=\"Reference\" loading=\"lazy\">\n </div>\n <div class=\"img-container img-after\">\n <div class=\"label\">After</div>\n <img src=\"${screenshots.after}\" alt=\"After\" loading=\"lazy\">\n </div>\n <div class=\"img-container img-diff\">\n <div class=\"label\">Diff</div>\n <img src=\"${screenshots.diff}\" alt=\"Diff\" loading=\"lazy\">\n </div>\n </div>\n </div>`;\n\n // Passしたテストはアコーディオンに格納\n const screenshotSection =\n status === \"pass\"\n ? `<details class=\"screenshot-accordion\"><summary>Show</summary>${imagesHtml}</details>`\n : imagesHtml;\n\n return `\n <div class=\"card ${viewport.type}\">\n <div class=\"card-header\">\n <h3>${page.path} - ${vpLabel}</h3>\n <span class=\"badge ${status}\">${status === \"error\" ? \"ERROR\" : `${status.toUpperCase()} ${comparison.diffPercentage.toFixed(2)}%`}</span>\n </div>\n ${screenshotSection}\n </div>`;\n}\n","<!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}} → 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","* {\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","import { mkdir, writeFile, access } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport skillTemplate from \"../templates/SKILL-TEMPLATE.md\";\n\nexport interface InitOptions {\n refDomain: string;\n}\n\n/**\n * .claude/skills/web-corders-vrt/SKILL.md スキルファイルを生成する。\n */\nexport async function runInit(options: InitOptions): Promise<void> {\n const outDir = join(process.cwd(), \".claude\", \"skills\", \"web-corders-vrt\");\n const outFile = join(outDir, \"SKILL.md\");\n\n // 既存ファイルの確認\n try {\n await access(outFile);\n console.log(chalk.yellow(`⚠ ${outFile} already exists. Overwriting...`));\n } catch {\n // ファイルが存在しない場合は正常\n }\n\n await mkdir(outDir, { recursive: true });\n\n const content = (skillTemplate as string).replace(\n /\\$\\{referenceDomain\\}/g,\n options.refDomain,\n );\n await writeFile(outFile, content, \"utf-8\");\n\n console.log(chalk.green(`✅ Created ${outFile}`));\n console.log(\"\");\n console.log(\n \"This skill file allows Claude Code to run VRT with the /vrt command.\",\n );\n console.log(\n \"Review and customize the file, then commit it to your repository.\",\n );\n}\n","---\nname: web-corders-vrt\ndescription: WEB用VRT(Visual Regression Test)ツール。本番ドメインとローカル開発またはプレビュードメインの間で、特定パスのビジュアル差分を検出する。\n---\n\n## 使い方\n\n```bash\nnpx web-corders-vrt run \\\n --reference ${referenceDomain} \\\n --after <開発環境のドメイン|ユーザーから指示がない場合はhttp://localhost:3000> \\\n --paths <テスト対象のパス(カンマ区切り)> \\\n```\n\n## 手順\n\n1. ユーザーから比較対象のドメインを指定された場合、それに従う。指示がない場合、afterは開発サーバーとし、起動していなければ `npm run dev` で起動して待機する。ここで起動したドメインとプロトコルをafterのドメインとする。\n2. 上記のコマンドでVRTを実行する(`--paths` はタスクに応じて設定する)\n3. `./vrt-results/` 内の最新ディレクトリにある `report.json` を読む\n4. `status: \"fail\"` のテスト結果に注目する\n5. 該当するdiff画像(`*--diff.png`)をReadツールで視覚的に確認する\n6. diff画像から修正すべきCSSやHTMLの場所を特定する\n7. ソースコードを修正する\n8. 再度VRTを実行して修正が反映されたことを確認する\n\n## オプション\n\n| オプション | 説明 |\n| -------------------- | ------------------------------------------- |\n| `--reference <url>` | リファレンスプロトコル+ドメイン(本番環境) |\n| `--after <url>` | 比較先Uプロトコル+ドメイン(開発環境) |\n| `--paths <paths>` | テスト対象のページパス(カンマ区切り) |\n| `--threshold <n>` | 差分許容率(%)。デフォルト: 0.1 |\n| `--hide <selectors>` | 非表示にするCSSセレクタ(カンマ区切り) |\n| `--no-open` | HTMLレポートをブラウザで開かない |\n"],"mappings":"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","names":["writeFile","join","chalk","writeFile","join","report_default","join","writeFile","report_default","join","chalk","writeFile","mkdir","writeFile","join","chalk","join","chalk","mkdir","writeFile"]}
1
+ {"version":3,"sources":["../../bin/vrt.ts","../../src/schemas.ts","../../src/commands/run.ts","../../src/core/screenshotter.ts","../../src/constants.ts","../../src/core/stabilizer.ts","../../src/core/comparator.ts","../../src/reporters/terminal.ts","../../src/reporters/json.ts","../../src/reporters/html.ts","../../src/templates/report.html","../../src/templates/report.css","../../src/commands/init.ts","../../src/templates/SKILL-TEMPLATE.md"],"sourcesContent":["import { Command } from \"commander\";\nimport { cliOptionsSchema } from \"../src/schemas.js\";\nimport { runVrt } from \"../src/commands/run.js\";\nimport { runInit } from \"../src/commands/init.js\";\nimport type { ResolvedOptions } from \"../src/types.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"vrt\")\n .description(\"Visual Regression Testing CLI - Compare web pages visually\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"run\")\n .description(\"Run visual regression tests\")\n .requiredOption(\"--reference <url>\", \"Reference URL (production)\")\n .requiredOption(\"--after <url>\", \"Comparison URL (local/staging)\")\n .requiredOption(\"--paths <paths>\", \"Page paths to compare (comma-separated)\")\n .option(\"--threshold <n>\", \"Diff tolerance percentage\", \"0.1\")\n .option(\"--hide <selectors>\", \"CSS selectors to hide (comma-separated)\")\n .action(async (rawOptions) => {\n try {\n const parsed = cliOptionsSchema.parse(rawOptions);\n\n const options: ResolvedOptions = {\n referenceUrl: parsed.reference,\n afterUrl: parsed.after,\n paths: parsed.paths,\n threshold: parsed.threshold,\n hideSelectors: parsed.hide,\n };\n\n const report = await runVrt(options);\n process.exit(report.summary.overallStatus === \"pass\" ? 0 : 1);\n } catch (error) {\n if (error instanceof Error) {\n console.error(`Error: ${error.message}`);\n } else {\n console.error(\"An unexpected error occurred\");\n }\n process.exit(2);\n }\n });\n\nprogram\n .command(\"init\")\n .description(\n \"Generate .claude/skills/web-corders-vrt/SKILL.md for this repository\",\n )\n .requiredOption(\"--refDomain <domain>\", \"Reference domain URL (production)\")\n .action(async (options) => {\n try {\n await runInit({ refDomain: options.refDomain });\n } catch (error) {\n if (error instanceof Error) {\n console.error(`Error: ${error.message}`);\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import { z } from \"zod\";\n\nexport const cliOptionsSchema = z.object({\n reference: z.string().url(\"--reference must be a valid URL\"),\n after: z.string().url(\"--after must be a valid URL\"),\n paths: z.string().transform((val) => val.split(\",\").map((p) => p.trim())),\n threshold: z.coerce.number().min(0).max(100).default(0.1),\n hide: z\n .string()\n .optional()\n .transform((val) => (val ? val.split(\",\").map((s) => s.trim()) : [])),\n});\n\nexport type CliOptions = z.input<typeof cliOptionsSchema>;\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport ora from \"ora\";\nimport chalk from \"chalk\";\nimport { Screenshotter } from \"../core/screenshotter.js\";\nimport { compareImages } from \"../core/comparator.js\";\nimport { printTerminalReport } from \"../reporters/terminal.js\";\nimport { writeJsonReport } from \"../reporters/json.js\";\nimport { writeHtmlReport } from \"../reporters/html.js\";\nimport {\n DEFAULT_SP_VIEWPORT,\n DEFAULT_PC_VIEWPORT,\n DEFAULT_OUT_DIR,\n DEFAULT_THRESHOLD,\n} from \"../constants.js\";\nimport type {\n ResolvedOptions,\n VrtReport,\n VrtTestResult,\n ViewportType,\n} from \"../types.js\";\n\nconst VIEWPORTS: ViewportType[] = [\"sp\", \"pc\"];\n\n/**\n * VRTのメイン実行コマンド。\n * スクリーンショット取得 → 比較 → レポート生成を一気に行う。\n */\nexport async function runVrt(options: ResolvedOptions): Promise<VrtReport> {\n const startTime = Date.now();\n\n // 出力ディレクトリを作成\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\n const runDir = join(DEFAULT_OUT_DIR, timestamp);\n const screenshotsDir = join(runDir, \"screenshots\");\n await mkdir(screenshotsDir, { recursive: true });\n\n const screenshotter = new Screenshotter();\n const spinner = ora(\"Initializing browser...\").start();\n\n try {\n await screenshotter.initialize();\n\n // Reference スクリーンショット取得\n spinner.text = `Taking screenshots of ${chalk.cyan(options.referenceUrl)}...`;\n const referenceScreenshots = await screenshotter.captureAll(\n options.referenceUrl,\n options.paths,\n VIEWPORTS,\n options.hideSelectors,\n );\n\n // After スクリーンショット取得\n spinner.text = `Taking screenshots of ${chalk.cyan(options.afterUrl)}...`;\n const afterScreenshots = await screenshotter.captureAll(\n options.afterUrl,\n options.paths,\n VIEWPORTS,\n options.hideSelectors,\n );\n\n spinner.text = \"Comparing screenshots...\";\n\n // 比較を実行\n const results: VrtTestResult[] = [];\n\n for (const referenceShot of referenceScreenshots) {\n const afterShot = afterScreenshots.find(\n (a) =>\n a.pagePath === referenceShot.pagePath &&\n a.viewportType === referenceShot.viewportType,\n );\n\n if (!afterShot) {\n results.push(\n createErrorResult(\n referenceShot.pagePath,\n referenceShot.viewportType,\n options,\n \"After screenshot not found\",\n ),\n );\n continue;\n }\n\n try {\n const comparison = compareImages(\n referenceShot.buffer,\n afterShot.buffer,\n options.threshold,\n );\n // スクリーンショットを保存\n const filePrefix = `${referenceShot.pageName}--${referenceShot.viewportType}`;\n const referencePath = join(\"screenshots\", `${filePrefix}--reference.png`);\n const afterPath = join(\"screenshots\", `${filePrefix}--after.png`);\n const diffPath = join(\"screenshots\", `${filePrefix}--diff.png`);\n\n await writeFile(join(runDir, referencePath), referenceShot.buffer);\n await writeFile(join(runDir, afterPath), afterShot.buffer);\n await writeFile(join(runDir, diffPath), comparison.diffImage);\n\n const pageName =\n referenceShot.pagePath === \"/\"\n ? \"トップページ\"\n : referenceShot.pagePath.replace(/^\\//, \"\");\n const viewport = getViewportDimensions(referenceShot.viewportType);\n\n results.push({\n page: {\n path: referenceShot.pagePath,\n name: pageName,\n url: {\n reference: new URL(\n referenceShot.pagePath,\n options.referenceUrl,\n ).toString(),\n after: new URL(referenceShot.pagePath, options.afterUrl).toString(),\n },\n },\n viewport: {\n type: referenceShot.viewportType,\n ...viewport,\n },\n status: comparison.passed ? \"pass\" : \"fail\",\n comparison: {\n diffPercentage: comparison.diffPercentage,\n diffPixelCount: comparison.diffCount,\n totalPixels: comparison.totalPixels,\n threshold: options.threshold,\n dimensions: comparison.dimensions,\n },\n screenshots: {\n reference: referencePath,\n after: afterPath,\n diff: diffPath,\n },\n });\n } catch (error) {\n results.push(\n createErrorResult(\n referenceShot.pagePath,\n referenceShot.viewportType,\n options,\n error instanceof Error ? error.message : String(error),\n ),\n );\n }\n }\n\n const duration = Date.now() - startTime;\n const passed = results.filter((r) => r.status === \"pass\").length;\n const failed = results.filter((r) => r.status === \"fail\").length;\n const errored = results.filter((r) => r.status === \"error\").length;\n\n const report: VrtReport = {\n version: \"1.0\",\n meta: {\n timestamp,\n referenceUrl: options.referenceUrl,\n afterUrl: options.afterUrl,\n duration,\n command: buildCommandString(options),\n },\n summary: {\n totalTests: results.length,\n passed,\n failed,\n errored,\n overallStatus: failed > 0 || errored > 0 ? \"fail\" : \"pass\",\n },\n results,\n };\n\n spinner.stop();\n\n // レポート出力\n printTerminalReport(report);\n\n const jsonPath = await writeJsonReport(report, runDir);\n console.log(`JSON report: ${chalk.dim(jsonPath)}`);\n\n const htmlPath = await writeHtmlReport(report, runDir);\n console.log(`HTML report: ${chalk.dim(htmlPath)}`);\n\n const { exec } = await import(\"node:child_process\");\n exec(`open \"${htmlPath}\"`);\n\n console.log(`Output dir: ${chalk.dim(runDir)}`);\n\n return report;\n } finally {\n await screenshotter.cleanup();\n }\n}\n\nfunction getViewportDimensions(\n type: ViewportType,\n): { width: number; height: number } {\n return type === \"sp\"\n ? { ...DEFAULT_SP_VIEWPORT }\n : { ...DEFAULT_PC_VIEWPORT };\n}\n\nfunction createErrorResult(\n pagePath: string,\n viewportType: ViewportType,\n options: ResolvedOptions,\n error: string,\n): VrtTestResult {\n const viewport = getViewportDimensions(viewportType);\n const pageName =\n pagePath === \"/\" ? \"トップページ\" : pagePath.replace(/^\\//, \"\");\n return {\n page: {\n path: pagePath,\n name: pageName,\n url: {\n reference: new URL(pagePath, options.referenceUrl).toString(),\n after: new URL(pagePath, options.afterUrl).toString(),\n },\n },\n viewport: { type: viewportType, ...viewport },\n status: \"error\",\n comparison: {\n diffPercentage: 0,\n diffPixelCount: 0,\n totalPixels: 0,\n threshold: options.threshold,\n dimensions: { width: 0, height: 0, referenceHeight: 0, afterHeight: 0 },\n },\n screenshots: { reference: \"\", after: \"\", diff: \"\" },\n error,\n };\n}\n\nfunction buildCommandString(options: ResolvedOptions): string {\n const parts = [\"npx web-corders-vrt run\"];\n parts.push(`--reference ${options.referenceUrl}`);\n parts.push(`--after ${options.afterUrl}`);\n parts.push(`--paths ${options.paths.join(\",\")}`);\n if (options.threshold !== DEFAULT_THRESHOLD) {\n parts.push(`--threshold ${options.threshold}`);\n }\n if (options.hideSelectors.length > 0) {\n parts.push(`--hide \"${options.hideSelectors.join(\",\")}\"`);\n }\n return parts.join(\" \\\\\\n \");\n}\n","import { chromium, type Browser, type BrowserContext } from \"playwright\";\nimport type { ScreenshotResult, ViewportType } from \"../types.js\";\nimport {\n DEFAULT_SP_VIEWPORT,\n DEFAULT_PC_VIEWPORT,\n DEFAULT_DELAY,\n DEFAULT_CONCURRENCY,\n SP_USER_AGENT,\n BLOCKED_DOMAINS,\n} from \"../constants.js\";\nimport { stabilizePage, getDateMockScript } from \"./stabilizer.js\";\n\nexport class Screenshotter {\n private browser: Browser | null = null;\n\n async initialize(): Promise<void> {\n this.browser = await chromium.launch();\n }\n\n /**\n * 指定URLのスクリーンショットを取得する。\n */\n async capture(\n url: string,\n pagePath: string,\n viewportType: ViewportType,\n hideSelectors: string[],\n ): Promise<ScreenshotResult> {\n if (!this.browser) {\n throw new Error(\"Browser not initialized. Call initialize() first.\");\n }\n\n const viewport =\n viewportType === \"sp\"\n ? { ...DEFAULT_SP_VIEWPORT }\n : { ...DEFAULT_PC_VIEWPORT };\n\n const context: BrowserContext = await this.browser.newContext({\n viewport,\n deviceScaleFactor: viewportType === \"sp\" ? 2 : 1,\n userAgent: viewportType === \"sp\" ? SP_USER_AGENT : undefined,\n });\n\n const page = await context.newPage();\n\n // 日付をモック\n await page.addInitScript(getDateMockScript());\n\n // 広告・計測系リクエストをブロック\n await page.route(\"**/*\", (route) => {\n const reqUrl = route.request().url();\n const shouldBlock = BLOCKED_DOMAINS.some((domain) =>\n reqUrl.includes(domain),\n );\n if (shouldBlock) {\n return route.abort();\n }\n return route.continue();\n });\n\n // ページにアクセス\n await page.goto(url, { waitUntil: \"load\", timeout: 60000 });\n\n // ページの安定化\n await stabilizePage(page, {\n hideSelectors,\n delay: DEFAULT_DELAY,\n });\n\n // スクリーンショット取得\n const buffer = await page.screenshot({\n fullPage: true,\n type: \"png\",\n });\n\n // 画像サイズを取得(PNGヘッダから読む)\n const width = buffer.readUInt32BE(16);\n const height = buffer.readUInt32BE(20);\n\n await context.close();\n\n // パスからページ名を生成\n const pageName =\n pagePath === \"/\"\n ? \"top\"\n : pagePath.replace(/^\\//, \"\").replace(/\\//g, \"-\");\n\n return {\n pagePath,\n pageName,\n viewportType,\n buffer: Buffer.from(buffer),\n width,\n height,\n };\n }\n\n /**\n * 複数のページ・ビューポートのスクリーンショットを並行で取得する。\n */\n async captureAll(\n baseUrl: string,\n paths: string[],\n viewports: ViewportType[],\n hideSelectors: string[],\n ): Promise<ScreenshotResult[]> {\n const tasks: Array<{ url: string; path: string; viewport: ViewportType }> =\n [];\n\n for (const pagePath of paths) {\n const url = new URL(pagePath, baseUrl).toString();\n for (const viewport of viewports) {\n tasks.push({ url, path: pagePath, viewport });\n }\n }\n\n // 並行実行制御\n const results: ScreenshotResult[] = [];\n\n for (let i = 0; i < tasks.length; i += DEFAULT_CONCURRENCY) {\n const batch = tasks.slice(i, i + DEFAULT_CONCURRENCY);\n const batchResults = await Promise.all(\n batch.map((task) =>\n this.capture(task.url, task.path, task.viewport, hideSelectors),\n ),\n );\n results.push(...batchResults);\n }\n\n return results;\n }\n\n async cleanup(): Promise<void> {\n await this.browser?.close();\n this.browser = null;\n }\n}\n","export const DEFAULT_SP_VIEWPORT = { width: 375, height: 812 } as const;\nexport const DEFAULT_PC_VIEWPORT = { width: 1440, height: 900 } as const;\n\nexport const DEFAULT_THRESHOLD = 0.1;\nexport const DEFAULT_DELAY = 1000;\nexport const DEFAULT_CONCURRENCY = 3;\nexport const DEFAULT_OUT_DIR = \"./vrt-results\";\n\nexport const SP_USER_AGENT =\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\";\n\n/** VRTに不要な広告・計測系ドメイン(常にブロック) */\nexport const BLOCKED_DOMAINS = [\n \"googletagmanager.com\",\n \"google-analytics.com\",\n \"googleadservices.com\",\n \"googlesyndication.com\",\n \"doubleclick.net\",\n \"facebook.net\",\n \"fbcdn.net\",\n \"analytics.yahoo.co.jp\",\n \"clarity.ms\",\n \"hotjar.com\",\n \"newrelic.com\",\n \"sentry.io\",\n \"datadoghq.com\",\n];\n\n/** VRTに不要な開発ツール系要素(常に非表示) */\nexport const DEFAULT_HIDE_SELECTORS = [\"#devtools-indicator\"];\n\n/** アニメーション・トランジションを無効化するCSS */\nexport const DISABLE_ANIMATIONS_CSS = `\n*, *::before, *::after {\n animation-duration: 0s !important;\n animation-delay: 0s !important;\n transition-duration: 0s !important;\n transition-delay: 0s !important;\n scroll-behavior: auto !important;\n caret-color: transparent !important;\n}\n`;\n","import type { Page } from \"playwright\";\nimport {\n DISABLE_ANIMATIONS_CSS,\n DEFAULT_HIDE_SELECTORS,\n} from \"../constants.js\";\n\nexport interface StabilizeOptions {\n disableAnimations?: boolean;\n hideSelectors?: string[];\n delay?: number;\n}\n\n/**\n * ページの動的コンテンツを安定化させる。\n * アニメーション無効化、要素の非表示、日付のモックなどを行う。\n */\nexport async function stabilizePage(\n page: Page,\n options: StabilizeOptions,\n): Promise<void> {\n // アニメーション・トランジションを無効化\n if (options.disableAnimations !== false) {\n await page.addStyleTag({ content: DISABLE_ANIMATIONS_CSS });\n }\n\n // 指定要素を非表示にする(visibility: hidden でレイアウトを崩さない)\n const allHideSelectors = [\n ...DEFAULT_HIDE_SELECTORS,\n ...(options.hideSelectors ?? []),\n ];\n if (allHideSelectors.length > 0) {\n const hideCSS = allHideSelectors\n .map((s) => `${s} { visibility: hidden !important; }`)\n .join(\"\\n\");\n await page.addStyleTag({ content: hideCSS });\n }\n\n // 追加の待機時間(hydration完了を待つ)\n if (options.delay && options.delay > 0) {\n await page.waitForTimeout(options.delay);\n }\n\n // Shadow DOM内の要素はCSSが届かないのでJSで削除(Next.js devtools等)\n if (allHideSelectors.length > 0) {\n await page.evaluate(`\n for (const sel of ${JSON.stringify(allHideSelectors)}) {\n document.querySelectorAll(sel).forEach(el => el.remove());\n document.querySelectorAll(\"*\").forEach(el => {\n if (el.shadowRoot) {\n el.shadowRoot.querySelectorAll(sel).forEach(inner => inner.remove());\n }\n });\n }\n `);\n }\n}\n\n/**\n * 日付をモックして固定値にする初期化スクリプト。\n * page.goto() の前に page.addInitScript() で使う。\n */\nexport function getDateMockScript(): string {\n return `\n (() => {\n const fixedDate = new Date('2025-01-01T00:00:00Z');\n const OrigDate = Date;\n const MockDate = class extends OrigDate {\n constructor(...args) {\n if (args.length === 0) {\n super(fixedDate.getTime());\n } else {\n super(...args);\n }\n }\n static now() { return fixedDate.getTime(); }\n };\n globalThis.Date = MockDate;\n })();\n `;\n}\n","import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport type { ComparisonResult } from \"../types.js\";\n\n/**\n * 2つのPNG画像をピクセル単位で比較し、差分画像を生成する。\n * サイズが異なる場合は大きい方に合わせて白パディングで拡張する。\n */\nexport function compareImages(\n referenceBuffer: Buffer,\n afterBuffer: Buffer,\n threshold: number = 0.1,\n): ComparisonResult {\n const reference = PNG.sync.read(referenceBuffer);\n const after = PNG.sync.read(afterBuffer);\n\n const width = Math.max(reference.width, after.width);\n const height = Math.max(reference.height, after.height);\n\n // サイズが異なる場合は正規化\n const normalizedReference = normalizeImage(reference, width, height);\n const normalizedAfter = normalizeImage(after, width, height);\n\n const diff = new PNG({ width, height });\n\n const diffCount = pixelmatch(\n normalizedReference.data,\n normalizedAfter.data,\n diff.data,\n width,\n height,\n {\n threshold: 0.1,\n includeAA: false,\n alpha: 0.1,\n diffColor: [255, 0, 0],\n diffColorAlt: [0, 200, 0],\n },\n );\n\n const totalPixels = width * height;\n const diffPercentage = (diffCount / totalPixels) * 100;\n\n return {\n diffCount,\n totalPixels,\n diffPercentage,\n diffImage: PNG.sync.write(diff),\n passed: diffPercentage <= threshold,\n dimensions: {\n width,\n height,\n referenceHeight: reference.height,\n afterHeight: after.height,\n },\n };\n}\n\n/**\n * 画像を指定サイズに正規化する。\n * 小さい場合は白(#ffffff)でパディングする。\n */\nfunction normalizeImage(\n png: PNG,\n targetWidth: number,\n targetHeight: number,\n): PNG {\n if (png.width === targetWidth && png.height === targetHeight) {\n return png;\n }\n\n const normalized = new PNG({ width: targetWidth, height: targetHeight });\n\n // 白で埋める\n for (let i = 0; i < normalized.data.length; i += 4) {\n normalized.data[i] = 255; // R\n normalized.data[i + 1] = 255; // G\n normalized.data[i + 2] = 255; // B\n normalized.data[i + 3] = 255; // A\n }\n\n // 元の画像をコピー\n for (let y = 0; y < png.height; y++) {\n for (let x = 0; x < png.width; x++) {\n const srcIdx = (y * png.width + x) * 4;\n const dstIdx = (y * targetWidth + x) * 4;\n normalized.data[dstIdx] = png.data[srcIdx];\n normalized.data[dstIdx + 1] = png.data[srcIdx + 1];\n normalized.data[dstIdx + 2] = png.data[srcIdx + 2];\n normalized.data[dstIdx + 3] = png.data[srcIdx + 3];\n }\n }\n\n return normalized;\n}\n","import chalk from \"chalk\";\nimport type { VrtReport } from \"../types.js\";\n\n/**\n * ターミナルにカラー出力でVRT結果を表示する。\n */\nexport function printTerminalReport(report: VrtReport): void {\n const { meta, summary, results } = report;\n\n console.log(\"\");\n console.log(chalk.bold(`VRT Results - ${meta.timestamp}`));\n console.log(\"=\".repeat(50));\n console.log(\"\");\n console.log(\n `Comparing: ${chalk.cyan(meta.referenceUrl)} vs ${chalk.cyan(meta.afterUrl)}`,\n );\n console.log(\"\");\n\n // ページごとにグループ化\n const groupedByPage = new Map<string, typeof results>();\n for (const result of results) {\n const key = result.page.path;\n if (!groupedByPage.has(key)) {\n groupedByPage.set(key, []);\n }\n groupedByPage.get(key)!.push(result);\n }\n\n for (const [pagePath, pageResults] of groupedByPage) {\n const pageName = pageResults[0].page.name;\n console.log(` Page: ${chalk.bold(pageName)} (${pagePath})`);\n\n for (const result of pageResults) {\n const vpLabel = `${result.viewport.type.toUpperCase()} (${result.viewport.width}x${result.viewport.height})`;\n\n if (result.status === \"error\") {\n console.log(\n ` ${vpLabel} ${chalk.yellow(\"⚠ ERROR\")} ${result.error || \"Unknown error\"}`,\n );\n continue;\n }\n\n const diffStr = `diff: ${result.comparison.diffPercentage.toFixed(2)}%`;\n\n if (result.status === \"pass\") {\n console.log(\n ` ${vpLabel} ${chalk.green(\"✅ PASS\")} ${chalk.dim(diffStr)}`,\n );\n } else {\n const thresholdStr = `(threshold: ${result.comparison.threshold}%)`;\n console.log(\n ` ${vpLabel} ${chalk.red(\"❌ FAIL\")} ${chalk.red(diffStr)} ${chalk.dim(thresholdStr)}`,\n );\n\n }\n }\n\n console.log(\"\");\n }\n\n // サマリー\n const statusColor =\n summary.overallStatus === \"pass\" ? chalk.green : chalk.red;\n console.log(\n `Summary: ${chalk.green(`${summary.passed} passed`)}, ${summary.failed > 0 ? chalk.red(`${summary.failed} failed`) : `${summary.failed} failed`}${summary.errored > 0 ? `, ${chalk.yellow(`${summary.errored} errored`)}` : \"\"} / ${summary.totalTests} total`,\n );\n console.log(`Overall: ${statusColor(summary.overallStatus.toUpperCase())}`);\n console.log(`Duration: ${(meta.duration / 1000).toFixed(1)}s`);\n console.log(\"\");\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { VrtReport } from \"../types.js\";\n\n/**\n * VrtReport を JSON ファイルとして保存する。\n */\nexport async function writeJsonReport(\n report: VrtReport,\n outDir: string,\n): Promise<string> {\n const filePath = join(outDir, \"report.json\");\n await writeFile(filePath, JSON.stringify(report, null, 2), \"utf-8\");\n return filePath;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { VrtReport, VrtTestResult } from \"../types.js\";\nimport reportTemplate from \"../templates/report.html\";\nimport reportStyles from \"../templates/report.css\";\n\n/**\n * 単一HTMLファイルのレポートを生成する。\n * 外部依存なし、CSSはインライン。\n */\nexport async function writeHtmlReport(\n report: VrtReport,\n outDir: string,\n): Promise<string> {\n const html = generateHtml(report);\n const filePath = join(outDir, \"report.html\");\n await writeFile(filePath, html, \"utf-8\");\n return filePath;\n}\n\nfunction generateHtml(report: VrtReport): string {\n const { meta, summary, results } = report;\n\n const resultCards = results.map((r) => generateResultCard(r)).join(\"\\n\");\n\n const failedStat =\n summary.failed > 0\n ? `<div class=\"stat fail\">${summary.failed} Failed</div>`\n : \"\";\n\n return (reportTemplate as string)\n .replace(\"{{CSS}}\", reportStyles as string)\n .replace(/\\{\\{TIMESTAMP\\}\\}/g, meta.timestamp)\n .replace(\"{{DURATION}}\", (meta.duration / 1000).toFixed(1))\n .replace(\"{{REFERENCE_URL}}\", meta.referenceUrl)\n .replace(\"{{AFTER_URL}}\", meta.afterUrl)\n .replace(\"{{TOTAL}}\", String(summary.totalTests))\n .replace(\"{{PASSED}}\", String(summary.passed))\n .replace(\"{{FAILED_STAT}}\", failedStat)\n .replace(\"{{RESULTS}}\", resultCards);\n}\n\nfunction generateResultCard(result: VrtTestResult): string {\n const { page, viewport, status, comparison, screenshots } = result;\n const vpLabel = viewport.type.toUpperCase();\n\n const imagesHtml = `\n <div class=\"comparison\">\n <div class=\"images\">\n <div class=\"img-container img-reference\">\n <div class=\"label\">Reference</div>\n <img src=\"${screenshots.reference}\" alt=\"Reference\" loading=\"lazy\">\n </div>\n <div class=\"img-container img-after\">\n <div class=\"label\">After</div>\n <img src=\"${screenshots.after}\" alt=\"After\" loading=\"lazy\">\n </div>\n <div class=\"img-container img-diff\">\n <div class=\"label\">Diff</div>\n <img src=\"${screenshots.diff}\" alt=\"Diff\" loading=\"lazy\">\n </div>\n </div>\n </div>`;\n\n // Passしたテストはアコーディオンに格納\n const screenshotSection =\n status === \"pass\"\n ? `<details class=\"screenshot-accordion\"><summary>Show</summary>${imagesHtml}</details>`\n : imagesHtml;\n\n return `\n <div class=\"card ${viewport.type}\">\n <div class=\"card-header\">\n <h3>${page.path} - ${vpLabel}</h3>\n <span class=\"badge ${status}\">${status === \"error\" ? \"ERROR\" : `${status.toUpperCase()} ${comparison.diffPercentage.toFixed(2)}%`}</span>\n </div>\n ${screenshotSection}\n </div>`;\n}\n","<!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}} → 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","* {\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","import { mkdir, writeFile, readFile, access } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport skillTemplate from \"../templates/SKILL-TEMPLATE.md\";\n\nexport interface InitOptions {\n refDomain: string;\n}\n\n/**\n * .claude/skills/web-corders-vrt/SKILL.md スキルファイルを生成する。\n */\nexport async function runInit(options: InitOptions): Promise<void> {\n const outDir = join(process.cwd(), \".claude\", \"skills\", \"web-corders-vrt\");\n const outFile = join(outDir, \"SKILL.md\");\n\n // 既存ファイルの確認\n try {\n await access(outFile);\n console.log(chalk.yellow(`⚠ ${outFile} already exists. Overwriting...`));\n } catch {\n // ファイルが存在しない場合は正常\n }\n\n await mkdir(outDir, { recursive: true });\n\n const content = (skillTemplate as string).replace(\n /\\$\\{referenceDomain\\}/g,\n options.refDomain,\n );\n await writeFile(outFile, content, \"utf-8\");\n\n console.log(chalk.green(`✅ Created ${outFile}`));\n\n // .gitignore に vrt-results/ を追加\n await ensureGitignore(process.cwd());\n\n console.log(\"\");\n console.log(\n \"Review and customize the skill file, then commit it to your repository.\",\n );\n}\n\nconst VRT_RESULTS_PATTERN = \"vrt-results/\";\n\nasync function ensureGitignore(cwd: string): Promise<void> {\n const gitignorePath = join(cwd, \".gitignore\");\n\n let content = \"\";\n try {\n content = await readFile(gitignorePath, \"utf-8\");\n } catch {\n // .gitignore が存在しない場合は新規作成\n }\n\n // 既に含まれていれば何もしない\n const lines = content.split(\"\\n\");\n if (lines.some((line) => line.trim() === VRT_RESULTS_PATTERN)) {\n return;\n }\n\n // 末尾に追加(末尾改行を保証)\n const separator = content.length > 0 && !content.endsWith(\"\\n\") ? \"\\n\" : \"\";\n await writeFile(\n gitignorePath,\n content + separator + VRT_RESULTS_PATTERN + \"\\n\",\n \"utf-8\",\n );\n console.log(chalk.green(`✅ Added '${VRT_RESULTS_PATTERN}' to .gitignore`));\n}\n","---\nname: web-corders-vrt\ndescription: WEB用VRT(Visual Regression Test)ツール。本番ドメインとローカル開発またはプレビュードメインの間で、特定パスのビジュアル差分を検出する。\n---\n\n## 使い方\n\n```bash\nnpx web-corders-vrt run \\\n --reference ${referenceDomain} \\\n --after <開発環境のドメイン|ユーザーから指示がない場合はhttp://localhost:3000> \\\n --paths <テスト対象のパス(カンマ区切り)> \\\n```\n\n## 手順\n\n1. ユーザーから比較対象のドメインを指定された場合、それに従う。指示がない場合、afterは開発サーバーとし、起動していなければ `npm run dev` で起動して待機する。ここで起動したドメインとプロトコルをafterのドメインとする。\n2. 上記のコマンドでVRTを実行する(`--paths` はタスクに応じて設定する)\n3. `./vrt-results/` 内の最新ディレクトリにある `report.json` を読む\n4. `status: \"fail\"` のテスト結果に注目する\n5. 該当するdiff画像(`*--diff.png`)をReadツールで視覚的に確認する\n6. diff画像から修正すべきCSSやHTMLの場所を特定できるなら修正にはいる。再度VRTを実行して修正が反映されたことを確認する\n7. 修正が不可能ならユーザーにその旨を伝え、レポートが出来たことを伝える\n\n## オプション\n\n| オプション | 説明 |\n| -------------------- | ------------------------------------------- |\n| `--reference <url>` | リファレンスプロトコル+ドメイン(本番環境) |\n| `--after <url>` | 比較先Uプロトコル+ドメイン(開発環境) |\n| `--paths <paths>` | テスト対象のページパス(カンマ区切り) |\n| `--threshold <n>` | 差分許容率(%)。デフォルト: 0.1 |\n| `--hide <selectors>` | 非表示にするCSSセレクタ(カンマ区切り) |\n| `--no-open` | HTMLレポートをブラウザで開かない |\n"],"mappings":"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","names":["writeFile","join","chalk","writeFile","join","report_default","join","writeFile","report_default","join","chalk","writeFile","mkdir","writeFile","join","chalk","join","chalk","mkdir","writeFile"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-corders-vrt",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Visual Regression Testing CLI tool for comparing web pages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile, access } from "node:fs/promises";
1
+ import { mkdir, writeFile, readFile, access } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import chalk from "chalk";
4
4
  import skillTemplate from "../templates/SKILL-TEMPLATE.md";
@@ -31,11 +31,40 @@ export async function runInit(options: InitOptions): Promise<void> {
31
31
  await writeFile(outFile, content, "utf-8");
32
32
 
33
33
  console.log(chalk.green(`✅ Created ${outFile}`));
34
+
35
+ // .gitignore に vrt-results/ を追加
36
+ await ensureGitignore(process.cwd());
37
+
34
38
  console.log("");
35
39
  console.log(
36
- "This skill file allows Claude Code to run VRT with the /vrt command.",
40
+ "Review and customize the skill file, then commit it to your repository.",
37
41
  );
38
- console.log(
39
- "Review and customize the file, then commit it to your repository.",
42
+ }
43
+
44
+ const VRT_RESULTS_PATTERN = "vrt-results/";
45
+
46
+ async function ensureGitignore(cwd: string): Promise<void> {
47
+ const gitignorePath = join(cwd, ".gitignore");
48
+
49
+ let content = "";
50
+ try {
51
+ content = await readFile(gitignorePath, "utf-8");
52
+ } catch {
53
+ // .gitignore が存在しない場合は新規作成
54
+ }
55
+
56
+ // 既に含まれていれば何もしない
57
+ const lines = content.split("\n");
58
+ if (lines.some((line) => line.trim() === VRT_RESULTS_PATTERN)) {
59
+ return;
60
+ }
61
+
62
+ // 末尾に追加(末尾改行を保証)
63
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
64
+ await writeFile(
65
+ gitignorePath,
66
+ content + separator + VRT_RESULTS_PATTERN + "\n",
67
+ "utf-8",
40
68
  );
69
+ console.log(chalk.green(`✅ Added '${VRT_RESULTS_PATTERN}' to .gitignore`));
41
70
  }
@@ -179,15 +179,11 @@ export async function runVrt(options: ResolvedOptions): Promise<VrtReport> {
179
179
  const jsonPath = await writeJsonReport(report, runDir);
180
180
  console.log(`JSON report: ${chalk.dim(jsonPath)}`);
181
181
 
182
- if (options.html) {
183
- const htmlPath = await writeHtmlReport(report, runDir);
184
- console.log(`HTML report: ${chalk.dim(htmlPath)}`);
182
+ const htmlPath = await writeHtmlReport(report, runDir);
183
+ console.log(`HTML report: ${chalk.dim(htmlPath)}`);
185
184
 
186
- if (options.open) {
187
- const { exec } = await import("node:child_process");
188
- exec(`open "${htmlPath}"`);
189
- }
190
- }
185
+ const { exec } = await import("node:child_process");
186
+ exec(`open "${htmlPath}"`);
191
187
 
192
188
  console.log(`Output dir: ${chalk.dim(runDir)}`);
193
189
 
package/src/constants.ts CHANGED
@@ -24,6 +24,7 @@ export const BLOCKED_DOMAINS = [
24
24
  "newrelic.com",
25
25
  "sentry.io",
26
26
  "datadoghq.com",
27
+ "cdn.treasuredata.com",
27
28
  ];
28
29
 
29
30
  /** VRTに不要な開発ツール系要素(常に非表示) */
package/src/schemas.ts CHANGED
@@ -9,8 +9,6 @@ export const cliOptionsSchema = z.object({
9
9
  .string()
10
10
  .optional()
11
11
  .transform((val) => (val ? val.split(",").map((s) => s.trim()) : [])),
12
- html: z.boolean().default(true),
13
- open: z.boolean().default(true),
14
12
  });
15
13
 
16
14
  export type CliOptions = z.input<typeof cliOptionsSchema>;
@@ -19,9 +19,8 @@ npx web-corders-vrt run \
19
19
  3. `./vrt-results/` 内の最新ディレクトリにある `report.json` を読む
20
20
  4. `status: "fail"` のテスト結果に注目する
21
21
  5. 該当するdiff画像(`*--diff.png`)をReadツールで視覚的に確認する
22
- 6. diff画像から修正すべきCSSやHTMLの場所を特定する
23
- 7. ソースコードを修正する
24
- 8. 再度VRTを実行して修正が反映されたことを確認する
22
+ 6. diff画像から修正すべきCSSやHTMLの場所を特定できるなら修正にはいる。再度VRTを実行して修正が反映されたことを確認する
23
+ 7. 修正が不可能ならユーザーにその旨を伝え、レポートが出来たことを伝える
25
24
 
26
25
  ## オプション
27
26
 
package/src/types.ts CHANGED
@@ -64,8 +64,6 @@ export interface ResolvedOptions {
64
64
  paths: string[];
65
65
  threshold: number;
66
66
  hideSelectors: string[];
67
- html: boolean;
68
- open: boolean;
69
67
  }
70
68
 
71
69
  /** スクリーンショット結果 */