Skip to content

Commit 70f70b8

Browse files
test: add end-to-end integration tests for full pipeline
Tests the complete DojoWatch flow against a live HTTP server: - Playwright captures a real HTML page at configured viewport - Baseline promotion copies captures correctly - Prefilter returns SKIP for identical screenshots - Prefilter detects FULL_ANALYSIS after page modification (48% diff) - Comment generator produces valid markdown from check runs 37 tests total, all passing (~2s for integration suite). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dcb4a4a commit 70f70b8

File tree

2 files changed

+197
-6
lines changed

2 files changed

+197
-6
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ Source change → Playwright capture → pixelmatch pre-filter → AI analysis
2020
### As a Claude Code plugin
2121

2222
```bash
23-
# From the marketplace (coming soon)
24-
/plugin install dojowatch
25-
26-
# From GitHub
23+
# From GitHub (recommended)
2724
claude plugin add --git https://github.com/DojoCodingLabs/dojowatch
25+
26+
# Local development
27+
claude --plugin-dir /path/to/dojowatch
2828
```
2929

30-
### For CI only
30+
### For CI
3131

32-
Clone or add as a git submodule in your project. The CI scripts run standalone via `npx tsx`.
32+
Clone or add as a git submodule. The CI scripts run standalone via `npx tsx`.
3333

3434
## Quick start
3535

tests/integration.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* Integration tests for DojoWatch pipeline.
3+
*
4+
* Tests the full flow: config → capture → prefilter → comment generation.
5+
* Uses a minimal static HTML page served by a local HTTP server.
6+
*/
7+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
8+
import { createServer, type Server } from "node:http";
9+
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node:fs";
10+
import { join } from "node:path";
11+
import { captureRoutes } from "../scripts/capture.js";
12+
import { prefilterAll } from "../scripts/prefilter.js";
13+
import { promoteToBaseline } from "../scripts/baseline.js";
14+
import { generateCommentMarkdown } from "../scripts/comment.js";
15+
import type { DojoWatchConfig, CheckRun } from "../scripts/types.js";
16+
17+
const TEST_DIR = join(import.meta.dirname, ".tmp-integration");
18+
const PORT = 9876;
19+
let server: Server;
20+
21+
const HTML_PAGE = `<!DOCTYPE html>
22+
<html>
23+
<head><style>body { margin: 0; font-family: sans-serif; background: #1a1a2e; color: white; }
24+
.header { padding: 16px; background: #16213e; } .content { padding: 32px; }
25+
.card { background: #0f3460; border-radius: 8px; padding: 24px; margin: 16px 0; }
26+
</style></head>
27+
<body>
28+
<div class="header"><h1>DojoWatch Test</h1></div>
29+
<div class="content">
30+
<div class="card"><h2>Dashboard</h2><p>Visual regression testing works.</p></div>
31+
</div>
32+
</body>
33+
</html>`;
34+
35+
const MODIFIED_HTML = `<!DOCTYPE html>
36+
<html>
37+
<head><style>body { margin: 0; font-family: sans-serif; background: #1a1a2e; color: white; }
38+
.header { padding: 24px; background: #e94560; } .content { padding: 32px; }
39+
.card { background: #0f3460; border-radius: 8px; padding: 24px; margin: 16px 0; }
40+
</style></head>
41+
<body>
42+
<div class="header"><h1>DojoWatch Test — Modified</h1></div>
43+
<div class="content">
44+
<div class="card"><h2>Dashboard</h2><p>Visual regression testing works.</p></div>
45+
<div class="card"><h2>New Feature</h2><p>This card was added.</p></div>
46+
</div>
47+
</body>
48+
</html>`;
49+
50+
let currentHTML = HTML_PAGE;
51+
52+
const config: DojoWatchConfig = {
53+
project: "integration-test",
54+
baseUrl: `http://localhost:${PORT}`,
55+
viewports: [{ name: "desktop", width: 800, height: 600 }],
56+
routes: ["/"],
57+
maskSelectors: [],
58+
engine: {
59+
local: { model: "claude" },
60+
ci: { model: "gemini-3.1-pro-preview", apiKeyEnv: "GOOGLE_GENAI_API_KEY" },
61+
},
62+
prefilter: { threshold: 0.05, clusterMinPixels: 500 },
63+
};
64+
65+
beforeAll(async () => {
66+
// Set up test directory
67+
mkdirSync(join(TEST_DIR, ".dojowatch"), { recursive: true });
68+
writeFileSync(
69+
join(TEST_DIR, ".dojowatch", "config.json"),
70+
JSON.stringify(config)
71+
);
72+
73+
// Start a minimal HTTP server
74+
server = createServer((_req, res) => {
75+
res.writeHead(200, { "Content-Type": "text/html" });
76+
res.end(currentHTML);
77+
});
78+
79+
await new Promise<void>((resolve) => {
80+
server.listen(PORT, resolve);
81+
});
82+
});
83+
84+
afterAll(async () => {
85+
await new Promise<void>((resolve) => {
86+
server.close(() => resolve());
87+
});
88+
rmSync(TEST_DIR, { recursive: true, force: true });
89+
});
90+
91+
describe("DojoWatch integration pipeline", () => {
92+
it("captures screenshots from a live server", async () => {
93+
const capturesDir = join(TEST_DIR, ".dojowatch", "captures");
94+
const results = await captureRoutes(config, ["/"], capturesDir);
95+
96+
expect(results).toHaveLength(1);
97+
expect(results[0].name).toBe("index");
98+
expect(results[0].viewport).toBe("desktop");
99+
expect(existsSync(results[0].path)).toBe(true);
100+
// SHA-256 hash should be a 64-char hex string
101+
expect(results[0].hash).toMatch(/^[a-f0-9]{64}$/);
102+
});
103+
104+
it("promotes captures to baselines", () => {
105+
const { promoted } = promoteToBaseline(TEST_DIR);
106+
expect(promoted).toHaveLength(1);
107+
expect(promoted[0]).toBe("index-desktop.png");
108+
109+
const baselinesDir = join(TEST_DIR, ".dojowatch", "baselines");
110+
expect(existsSync(join(baselinesDir, "index-desktop.png"))).toBe(true);
111+
});
112+
113+
it("prefilter returns SKIP when captures match baselines", () => {
114+
const results = prefilterAll(TEST_DIR);
115+
expect(results).toHaveLength(1);
116+
expect(results[0].tier).toBe("SKIP");
117+
expect(results[0].pixelDiffCount).toBe(0);
118+
});
119+
120+
it("prefilter detects changes after modifying the page", async () => {
121+
// Switch to the modified page
122+
currentHTML = MODIFIED_HTML;
123+
124+
// Re-capture
125+
const capturesDir = join(TEST_DIR, ".dojowatch", "captures");
126+
await captureRoutes(config, ["/"], capturesDir);
127+
128+
// Run prefilter
129+
const results = prefilterAll(TEST_DIR);
130+
expect(results).toHaveLength(1);
131+
expect(results[0].tier).not.toBe("SKIP");
132+
expect(results[0].pixelDiffCount).toBeGreaterThan(0);
133+
134+
// Diff image should be generated
135+
const diffsDir = join(TEST_DIR, ".dojowatch", "diffs");
136+
const diffFiles = existsSync(diffsDir)
137+
? readdirSync(diffsDir).filter((f) => f.endsWith(".png"))
138+
: [];
139+
expect(diffFiles.length).toBeGreaterThan(0);
140+
});
141+
142+
it("generates a valid PR comment from a check run", () => {
143+
const prefilterResults = prefilterAll(TEST_DIR);
144+
145+
const checkRun: CheckRun = {
146+
timestamp: new Date().toISOString(),
147+
branch: "test-branch",
148+
commitSha: "abc1234567890",
149+
scope: "all",
150+
prefilterResults,
151+
analysisResults: [
152+
{
153+
name: "index-desktop",
154+
viewport: "desktop",
155+
tier: "FULL_ANALYSIS",
156+
diffs: [
157+
{
158+
element: "header background",
159+
type: "REGRESSION",
160+
severity: "medium",
161+
description: "Header changed from dark blue to red",
162+
suggested_fix: "Check .header background color in CSS",
163+
},
164+
{
165+
element: "new card section",
166+
type: "INTENTIONAL",
167+
description: "New feature card added below dashboard",
168+
},
169+
],
170+
},
171+
],
172+
summary: {
173+
total: 1,
174+
skipped: 0,
175+
analyzed: 1,
176+
regressions: 1,
177+
intentional: 1,
178+
noise: 0,
179+
},
180+
};
181+
182+
const markdown = generateCommentMarkdown(checkRun);
183+
184+
expect(markdown).toContain("DojoWatch Visual Regression Report");
185+
expect(markdown).toContain("header background");
186+
expect(markdown).toContain("❌ Regressions");
187+
expect(markdown).toContain("medium");
188+
expect(markdown).toContain("new card section");
189+
expect(markdown).toContain("Intentional changes");
190+
});
191+
});

0 commit comments

Comments
 (0)