diff --git a/package.json b/package.json index 3fb3623..ffcffb4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:unit": "tsx --test src/__tests__/unit/*.test.ts", "test:smoke": "npx playwright test --grep @smoke", "test:e2e": "npx playwright test", + "test:visual": "npx playwright test --grep @visual", "test": "npm run typecheck && npm run test:unit", "prepare": "node -e \"try{require('husky')()}catch(e){}\"", "electron:dev": "concurrently -k \"next dev\" \"wait-on http://localhost:3000 && electron .\"", diff --git a/playwright.config.ts b/playwright.config.ts index 6e1c936..7f58886 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,6 +11,11 @@ export default defineConfig({ baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.01, + }, + }, webServer: { command: 'npm run dev', url: 'http://localhost:3000', diff --git a/src/__tests__/e2e/visual-regression.spec.ts b/src/__tests__/e2e/visual-regression.spec.ts new file mode 100644 index 0000000..d097d6e --- /dev/null +++ b/src/__tests__/e2e/visual-regression.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; + +/** + * Visual regression tests for the design system and key pages. + * Run with: npm run test:visual + * + * Baseline snapshots are generated on first run. + * Update baselines with: npx playwright test --grep @visual --update-snapshots + */ + +test.describe('Visual Regression @visual', () => { + test('design system page', async ({ page }) => { + await page.goto('/design-system'); + await page.waitForLoadState('networkidle'); + + // Full page screenshot + await expect(page).toHaveScreenshot('design-system-full.png', { + fullPage: true, + maxDiffPixelRatio: 0.01, + }); + }); + + test('design system - buttons section', async ({ page }) => { + await page.goto('/design-system'); + await page.waitForLoadState('networkidle'); + + const section = page.locator('[data-section="Buttons"]'); + await expect(section).toHaveScreenshot('design-system-buttons.png', { + maxDiffPixelRatio: 0.01, + }); + }); + + test('design system - status banners section', async ({ page }) => { + await page.goto('/design-system'); + await page.waitForLoadState('networkidle'); + + const section = page.locator('[data-section="Status Banner"]'); + await expect(section).toHaveScreenshot('design-system-status-banners.png', { + maxDiffPixelRatio: 0.01, + }); + }); + + test('design system - settings card section', async ({ page }) => { + await page.goto('/design-system'); + await page.waitForLoadState('networkidle'); + + const section = page.locator('[data-section="Settings Card"]'); + await expect(section).toHaveScreenshot('design-system-settings-cards.png', { + maxDiffPixelRatio: 0.01, + }); + }); + + test('design system - field row section', async ({ page }) => { + await page.goto('/design-system'); + await page.waitForLoadState('networkidle'); + + const section = page.locator('[data-section="Field Row"]'); + await expect(section).toHaveScreenshot('design-system-field-rows.png', { + maxDiffPixelRatio: 0.01, + }); + }); + + test('settings page', async ({ page }) => { + await page.goto('/settings'); + await page.waitForLoadState('networkidle'); + // Wait for dynamic content to load + await page.waitForTimeout(1000); + + await expect(page).toHaveScreenshot('settings-page.png', { + fullPage: true, + maxDiffPixelRatio: 0.01, + }); + }); +}); diff --git a/src/app/design-system/layout.tsx b/src/app/design-system/layout.tsx new file mode 100644 index 0000000..9dd6a84 --- /dev/null +++ b/src/app/design-system/layout.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + +export default function DesignSystemLayout({ children }: { children: ReactNode }) { + return ( +
+
+
+

CodePilot Design System

+

+ Pattern components and UI primitives reference (dev only) +

+
+ {children} +
+
+ ); +} diff --git a/src/app/design-system/page.tsx b/src/app/design-system/page.tsx new file mode 100644 index 0000000..61a9c3b --- /dev/null +++ b/src/app/design-system/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { + SettingsCard, + FieldRow, + StatusBanner, + EmptyState, + SectionPage, + IconAction, +} from "@/components/patterns"; +import { + CheckCircle, + Warning, + Info, + Gear, + Trash, + Plus, + MagnifyingGlass, + Copy, +} from "@phosphor-icons/react"; + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +export default function DesignSystemPage() { + return ( + + {/* ── Buttons ── */} +
+
+ + + + + + + + + +
+
+ + {/* ── Icon Actions ── */} +
+
+ } tooltip="Settings" /> + } tooltip="Delete" /> + } tooltip="Copy" /> + } tooltip="Add" size="sm" /> + } tooltip="Search" size="sm" /> +
+
+ + {/* ── Inputs ── */} +
+
+ + + +
+
+ + {/* ── Badges ── */} +
+
+ Default + Secondary + Destructive + Outline +
+
+ + {/* ── Settings Card ── */} +
+ +

Card content goes here.

+
+ + +

Card without title — just content.

+
+ + +

With custom border color for active state.

+
+
+ + {/* ── Field Row ── */} +
+ + + + + + + + + + + +
+ + {/* ── Status Banner ── */} +
+
+ }> + Operation completed successfully. + + }> + This action cannot be undone. + + }> + Failed to save settings. Please try again. + + }> + New features are available in this version. + +
+
+ + {/* ── Empty State ── */} +
+ } + title="No results found" + description="Try adjusting your search terms or filters." + action={} + /> +
+ + {/* ── Section Page ── */} +
+

+ This entire page uses SectionPage maxWidth="lg" for consistent max-width and spacing. +

+
+
+ ); +}