Skip to content

Commit 482ac91

Browse files
feat: add authenticated route support via Playwright storageState
Captures pages behind login using Playwright's storageState: - auth.storageState — default auth file for all routes - auth.profiles — named profiles (admin, student, etc.) for role-based capture - auth.routes — per-route profile mapping (null = anonymous) - Routes grouped by auth profile to minimize browser context creation Generate auth state: npx playwright codegen --save-storage=.dojowatch/auth.json For CI, use a setup script (Supabase, Clerk, NextAuth) that saves state before capture. Integration test verifies anonymous vs authenticated capture produce different screenshots. 46 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e04740b commit 482ac91

12 files changed

Lines changed: 265 additions & 17 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
33
"name": "dojowatch",
4-
"version": "0.4.0",
4+
"version": "0.5.0",
55
"description": "AI-native visual regression testing engine by Dojo Coding — capture screenshots with Playwright, pre-filter with pixelmatch, and analyze UI changes with LLM vision. Zero incremental cost.",
66
"owner": {
77
"name": "Dojo Coding",
@@ -11,7 +11,7 @@
1111
{
1212
"name": "dojowatch",
1313
"description": "Visual regression testing powered by AI. Captures screenshots with Playwright, pre-filters with pixelmatch, and uses Claude (locally) or Gemini (in CI) to classify visual changes as regressions, intentional changes, or noise. Includes slash commands for interactive workflows and a CI orchestrator for GitHub Actions.",
14-
"version": "0.4.0",
14+
"version": "0.5.0",
1515
"author": {
1616
"name": "Dojo Coding",
1717
"email": "team@dojocoding.io"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dojowatch",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "AI-native visual regression testing engine by Dojo Coding — capture screenshots with Playwright, pre-filter with pixelmatch, and analyze UI changes with LLM vision. Zero incremental cost.",
55
"author": {
66
"name": "Dojo Coding",

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ dist/
77
# DojoWatch runtime artifacts (captures and diffs are ephemeral)
88
.dojowatch/captures/
99
.dojowatch/diffs/
10+
.dojowatch/auth*.json
11+
.dojowatch/last-check.json
12+
.dojowatch/prefilter-report.json
1013

1114
# Environment
1215
.env

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Changelog
22

3-
## [0.4.0] — Unreleased
3+
## [0.5.0] — Unreleased
4+
5+
### Added
6+
- **Authenticated route support**: Capture pages behind login using Playwright `storageState`
7+
- `auth.storageState` — default auth file for all routes
8+
- `auth.profiles` — named profiles (admin, student, etc.) mapping to different auth state files
9+
- `auth.routes` — per-route profile assignment (null = anonymous)
10+
- Routes grouped by auth profile to minimize browser context creation
11+
- Generate auth state: `npx playwright codegen --save-storage=.dojowatch/auth.json`
12+
- Auth state files (`.dojowatch/auth*.json`) added to `.gitignore`
13+
14+
## [0.4.0]
415

516
### Added
617
- **Supabase data layer** (`scripts/supabase.ts`): Full Supabase integration for shared storage

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,35 @@ Add `data-vr-mask` to elements that change between captures — timestamps, avat
194194

195195
DojoWatch replaces masked elements with solid placeholders before capture, preventing false positives.
196196

197+
### Authenticated Routes
198+
199+
Most apps have protected routes (dashboards, admin panels, settings). DojoWatch supports Playwright's `storageState` to capture these pages as a logged-in user:
200+
201+
```json
202+
{
203+
"auth": {
204+
"storageState": ".dojowatch/auth.json",
205+
"profiles": {
206+
"admin": "e2e/.auth/admin.json",
207+
"student": "e2e/.auth/student.json"
208+
},
209+
"routes": {
210+
"/": null,
211+
"/dashboard": "student",
212+
"/admin": "admin"
213+
}
214+
}
215+
}
216+
```
217+
218+
Generate an auth state file by logging in manually:
219+
220+
```bash
221+
npx playwright codegen --save-storage=.dojowatch/auth.json http://localhost:3000
222+
```
223+
224+
For CI, use a setup script that authenticates via your provider's API (Supabase, Clerk, Auth.js) and saves the state file before DojoWatch runs.
225+
197226
### Storybook Support
198227

199228
Point `storybookUrl` at your Storybook instance. DojoWatch crawls `stories.json`, captures every story in isolation, and provides component-level regression detection — equivalent to Chromatic's core offering.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dojowatch",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

scripts/capture.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import pc from "picocolors";
66
import { injectStabilization, maskElements } from "./stabilize.js";
77
import { loadConfig, findProjectRoot, getDojoWatchDir } from "./config.js";
88
import { loadRouteMap, resolveScope } from "./route-map.js";
9-
import type { CaptureResult, DojoWatchConfig, Viewport } from "./types.js";
9+
import type { AuthConfig, CaptureResult, DojoWatchConfig, Viewport } from "./types.js";
1010

1111
/**
1212
* Derive a filesystem-safe name from a route path.
@@ -69,8 +69,33 @@ async function captureRoute(
6969
};
7070
}
7171

72+
/**
73+
* Resolve which storageState file to use for a given route.
74+
* Returns undefined for anonymous access.
75+
*/
76+
function resolveAuthForRoute(
77+
route: string,
78+
auth?: AuthConfig
79+
): string | undefined {
80+
if (!auth) return undefined;
81+
82+
// Check per-route mapping first
83+
if (auth.routes && route in auth.routes) {
84+
const profileName = auth.routes[route];
85+
if (profileName === null) return undefined; // explicitly anonymous
86+
if (auth.profiles && profileName in auth.profiles) {
87+
return auth.profiles[profileName];
88+
}
89+
return undefined;
90+
}
91+
92+
// Fall back to default storageState
93+
return auth.storageState;
94+
}
95+
7296
/**
7397
* Capture all configured routes at all viewports.
98+
* Supports authenticated captures via Playwright storageState.
7499
*/
75100
export async function captureRoutes(
76101
config: DojoWatchConfig,
@@ -82,21 +107,39 @@ export async function captureRoutes(
82107
const browser = await chromium.launch({ headless: true });
83108
const results: CaptureResult[] = [];
84109

110+
// Group routes by auth profile to minimize context creation
111+
const routesByAuth = new Map<string | undefined, string[]>();
112+
for (const route of routes) {
113+
const authFile = resolveAuthForRoute(route, config.auth);
114+
const key = authFile ?? "__anonymous__";
115+
if (!routesByAuth.has(key)) routesByAuth.set(key, []);
116+
routesByAuth.get(key)!.push(route);
117+
}
118+
85119
try {
86-
const context = await browser.newContext();
87-
const page = await context.newPage();
120+
for (const [authKey, groupedRoutes] of routesByAuth) {
121+
const storageState = authKey === "__anonymous__" ? undefined : authKey;
122+
const context = await browser.newContext(
123+
storageState ? { storageState } : undefined
124+
);
125+
const page = await context.newPage();
126+
127+
if (storageState) {
128+
console.log(pc.dim(` Auth: ${storageState}`));
129+
}
88130

89-
for (const route of routes) {
90-
for (const viewport of config.viewports) {
91-
console.log(
92-
pc.dim(` Capturing ${route} @ ${viewport.name} (${viewport.width}x${viewport.height})`)
93-
);
94-
const result = await captureRoute(page, config, route, viewport, outputDir);
95-
results.push(result);
131+
for (const route of groupedRoutes) {
132+
for (const viewport of config.viewports) {
133+
console.log(
134+
pc.dim(` Capturing ${route} @ ${viewport.name} (${viewport.width}x${viewport.height})`)
135+
);
136+
const result = await captureRoute(page, config, route, viewport, outputDir);
137+
results.push(result);
138+
}
96139
}
97-
}
98140

99-
await context.close();
141+
await context.close();
142+
}
100143
} finally {
101144
await browser.close();
102145
}

scripts/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ function mergeConfig(
100100
...defaults.prefilter,
101101
...overrides.prefilter,
102102
},
103+
auth: overrides.auth
104+
? {
105+
storageState: overrides.auth.storageState,
106+
profiles: overrides.auth.profiles,
107+
routes: overrides.auth.routes,
108+
}
109+
: undefined,
103110
supabase: overrides.supabase
104111
? {
105112
url: overrides.supabase.url,

scripts/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export interface PrefilterConfig {
2626
clusterMinPixels: number;
2727
}
2828

29+
export interface AuthConfig {
30+
/** Default Playwright storageState file for authenticated captures. */
31+
storageState?: string;
32+
/** Named auth profiles mapping to storageState files (e.g., { "admin": "e2e/.auth/admin.json" }). */
33+
profiles?: Record<string, string>;
34+
/** Maps routes to profile names. null = anonymous (no auth). Unlisted routes use default storageState. */
35+
routes?: Record<string, string | null>;
36+
}
37+
2938
export interface SupabaseConfig {
3039
/** Supabase project URL. Read from env var in CI, .env.local locally. */
3140
url: string;
@@ -54,6 +63,8 @@ export interface DojoWatchConfig {
5463
engine: EngineConfig;
5564
/** Pre-filter configuration. */
5665
prefilter: PrefilterConfig;
66+
/** Authentication configuration. Optional — when absent, captures run as anonymous. */
67+
auth?: AuthConfig;
5768
/** Supabase configuration. Optional — when absent, local file storage is used. */
5869
supabase?: SupabaseConfig;
5970
}

skills/visual-regression/SKILL.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,39 @@ DojoWatch is configured via `.dojowatch/config.json`:
5151
- `/vr-report` — Generate markdown summary of last check
5252
- `/vr-watch` — File watcher with live re-capture (coming soon)
5353

54+
## Authenticated Routes
55+
56+
DojoWatch supports capturing pages behind authentication via Playwright's `storageState`:
57+
58+
```json
59+
{
60+
"auth": {
61+
"storageState": ".dojowatch/auth.json",
62+
"profiles": {
63+
"admin": "e2e/.auth/admin.json",
64+
"student": "e2e/.auth/student.json"
65+
},
66+
"routes": {
67+
"/": null,
68+
"/dashboard": "student",
69+
"/admin": "admin"
70+
}
71+
}
72+
}
73+
```
74+
75+
- **`storageState`** — default auth file for all routes (cookies + localStorage)
76+
- **`profiles`** — named profiles mapping to different auth state files
77+
- **`routes`** — per-route profile assignment. `null` = anonymous. Unlisted routes use the default.
78+
79+
To generate an auth state file:
80+
```bash
81+
npx playwright codegen --save-storage=.dojowatch/auth.json http://localhost:3000
82+
```
83+
This opens a browser — log in manually, then close it. The session is saved.
84+
85+
For automated CI, use a setup script that logs in via your auth provider's API (Supabase, Clerk, NextAuth) and saves the storageState file. See DojoOS's `e2e/global-setup.ts` for an example.
86+
5487
## File Structure
5588
- `.dojowatch/config.json` — Project configuration
5689
- `.dojowatch/routeMap.json` — Source file → route mapping

0 commit comments

Comments
 (0)