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:
1 parent
cfc4db1507
commit
c153cf26e6
5 files changed
+270
No files matched your search
@@ -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 .\"",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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="lg"</code> for consistent max-width and spacing.
|
||||
</p>
|
||||
</Section>
|
||||
</SectionPage>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user