feat: add Design System page + Playwright visual regression tests (PR8)

Design System page (/design-system):
- Showcases all pattern components: SettingsCard, FieldRow, StatusBanner,
  EmptyState, IconAction, SectionPage
- Includes Button variants, Input, Select, Switch, Badge primitives
- Each section has data-section attribute for targeted screenshots

Visual regression tests (npm run test:visual):
- Full design system page screenshot
- Individual section screenshots (buttons, status banners, cards, field rows)
- Settings page screenshot
- Uses maxDiffPixelRatio: 0.01 for tight regression detection

Infrastructure:
- Add test:visual script to package.json
- Update playwright.config.ts with screenshot comparison defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-11 21:56:21 +08:00
1 parent cfc4db1507
commit c153cf26e6
5 files changed
+270

No files matched your search

+1
View File
@@ -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 .\"",
+5
View File
@@ -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',
@@ -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,
});
});
});
+17
View File
@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
export default function DesignSystemLayout({ children }: { children: ReactNode }) {
return (
<div className="min-h-screen bg-background text-foreground p-8">
<div className="max-w-4xl mx-auto">
<header className="mb-8">
<h1 className="text-2xl font-bold">CodePilot Design System</h1>
<p className="text-sm text-muted-foreground mt-1">
Pattern components and UI primitives reference (dev only)
</p>
</header>
{children}
</div>
</div>
);
}
+173
View File
@@ -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 (
<section className="space-y-4" data-section={title}>
<h2 className="text-lg font-semibold border-b border-border pb-2">{title}</h2>
{children}
</section>
);
}
export default function DesignSystemPage() {
return (
<SectionPage maxWidth="lg" className="space-y-12">
{/* ── Buttons ── */}
<Section title="Buttons">
<div className="flex flex-wrap gap-3">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
</div>
</Section>
{/* ── Icon Actions ── */}
<Section title="Icon Actions">
<div className="flex gap-3">
<IconAction icon={<Gear size={16} />} tooltip="Settings" />
<IconAction icon={<Trash size={16} />} tooltip="Delete" />
<IconAction icon={<Copy size={16} />} tooltip="Copy" />
<IconAction icon={<Plus size={16} />} tooltip="Add" size="sm" />
<IconAction icon={<MagnifyingGlass size={16} />} tooltip="Search" size="sm" />
</div>
</Section>
{/* ── Inputs ── */}
<Section title="Inputs">
<div className="space-y-3 max-w-md">
<Input placeholder="Default input" />
<Input type="password" placeholder="Password input" />
<Select defaultValue="option1">
<SelectTrigger>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
<SelectItem value="option3">Option 3</SelectItem>
</SelectContent>
</Select>
</div>
</Section>
{/* ── Badges ── */}
<Section title="Badges">
<div className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
</Section>
{/* ── Settings Card ── */}
<Section title="Settings Card">
<SettingsCard title="Card with title" description="This is a description for the card.">
<p className="text-sm text-muted-foreground">Card content goes here.</p>
</SettingsCard>
<SettingsCard>
<p className="text-sm">Card without title — just content.</p>
</SettingsCard>
<SettingsCard className="border-primary/50 bg-primary/5" title="Active state card">
<p className="text-sm text-muted-foreground">With custom border color for active state.</p>
</SettingsCard>
</Section>
{/* ── Field Row ── */}
<Section title="Field Row">
<SettingsCard>
<FieldRow label="Toggle setting" description="Enable or disable this feature">
<Switch />
</FieldRow>
<FieldRow label="Select option" description="Choose your preference" separator>
<Select defaultValue="auto">
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="manual">Manual</SelectItem>
</SelectContent>
</Select>
</FieldRow>
<FieldRow label="Text input" separator>
<Input placeholder="Enter value" className="w-[200px]" />
</FieldRow>
</SettingsCard>
</Section>
{/* ── Status Banner ── */}
<Section title="Status Banner">
<div className="space-y-3">
<StatusBanner variant="success" icon={<CheckCircle size={16} />}>
Operation completed successfully.
</StatusBanner>
<StatusBanner variant="warning" icon={<Warning size={16} />}>
This action cannot be undone.
</StatusBanner>
<StatusBanner variant="error" icon={<Warning size={16} />}>
Failed to save settings. Please try again.
</StatusBanner>
<StatusBanner variant="info" icon={<Info size={16} />}>
New features are available in this version.
</StatusBanner>
</div>
</Section>
{/* ── Empty State ── */}
<Section title="Empty State">
<EmptyState
icon={<MagnifyingGlass size={32} />}
title="No results found"
description="Try adjusting your search terms or filters."
action={<Button size="sm">Clear filters</Button>}
/>
</Section>
{/* ── Section Page ── */}
<Section title="Section Page (layout)">
<p className="text-sm text-muted-foreground">
This entire page uses <code className="text-xs bg-muted px-1 py-0.5 rounded">SectionPage maxWidth=&quot;lg&quot;</code> for consistent max-width and spacing.
</p>
</Section>
</SectionPage>
);
}