name: UI Verifier description: Checks merged PRs for design spec compliance when UI files changed. triggers: - context: projects: projectIds: - 019d8bf4-1ded-7317-be2f-555e8fb55ff9 pullRequest: webhookId: 019d8d97-eb6e-70c4-a97d-25952abbc5a7 events: - PULL_REQUEST_EVENT_MERGED action: limits: maxParallel: 1 maxTotal: 50 steps: - agent: prompt: | You are the UI Verifier. A PR was just merged. Check that the UI matches the design spec. The PR number is provided in the trigger context. ## Step 1 — Determine if verification is needed Read the merged PR diff: gh pr diff --name-only If no files match src/components/*, src/app/**/page.tsx, or src/app/**/layout.tsx — stop. This PR has no UI changes. ## Step 2 — Static analysis (code review against design spec) 1. Read `.agents/design.md` — this is the source of truth. 2. Read the full diff of changed UI files. Check every changed file against the design spec: | Check | What to look for | |---|---| | Color tokens | No arbitrary hex/rgb values. All colors use CSS variables from the design spec token set. | | Typography | Font sizes only from the typography scale (text-xs through text-3xl). No text-base for body. | | Spacing | Only Tailwind scale values. No arbitrary spacing (p-[13px]). | | Component usage | shadcn/ui components used correctly — right variant, right size for context. | | Button variants | default for primary, ghost for toolbar, destructive for delete. No misuse. | | Loading states | Skeletons (not spinners). Optimistic updates for mutations. | | Empty states | Centered, icon + heading + description + CTA. No bare "nothing here" text. | | Transitions | No decorative animations. Sidebar 200ms, dropdowns 150ms, page nav instant. | | Responsive | Mobile breakpoint handled. Touch targets ≥44px. No horizontal scroll. | | Accessibility | aria-label on icon buttons, alt on images, keyboard-accessible interactions. | | Dark mode | Uses token variables, not hardcoded light-only colors. | | Borders | 1px solid border-border. No box-shadows except dropdowns/modals. | ## Step 3 — Storybook visual regression Run visual regression tests against Storybook to catch component-level visual changes introduced by the PR. 1. Build Storybook: `pnpm build-storybook` 2. Serve the static build: `python3 -m http.server 6099 -d storybook-static &` 3. Run visual regression: `STORYBOOK_URL=http://localhost:6099 pnpm test:visual` 4. If tests fail, the PR introduced visual changes to existing components. Capture the diff images from `test-results/` for inclusion in the report. 5. Clean up: `kill $(lsof -ti:6099) 2>/dev/null; rm -rf storybook-static` Visual regression failures are NOT automatically blocking — they indicate intentional or unintentional visual changes. Include the diffs in the report so a human can determine if the changes are acceptable. ## Step 3b — Live site vs Storybook comparison This step catches integration-level visual bugs — components that render correctly in Storybook isolation but break in the real page context (e.g., layout constraints from parent containers, CSS cascade overrides, missing component wiring, portal/overlay issues). For each new or modified component in the PR diff: 1. Identify the corresponding Storybook story (from the `*.stories.tsx` file). 2. Screenshot the Storybook story at desktop viewport (1280×800) using the Storybook build from Step 3. 3. Screenshot the same component on the live site at the same viewport. Navigate to the page where the component appears, wait for it to render, and capture the relevant area. 4. Compare the two screenshots visually. Look for: - **Layout differences**: component constrained or stretched differently than in the story (e.g., max-width containers, flex/grid parents) - **Missing sub-components**: story shows the correct child component but live site renders a fallback (e.g., plain `` instead of DatePicker) - **Style overrides**: page-level CSS affecting component appearance - **Overflow/clipping**: component content cut off by parent containers - **Portal/overlay issues**: dropdowns, popovers, or modals not rendering correctly in the full page context If discrepancies are found, include them in the report with both screenshots and a description of the difference. These are high-priority findings because they indicate the component works in isolation but fails in production. Note: This comparison requires the Storybook build from Step 3 to still be running. Keep the server alive until this step completes, then clean up. ## Step 4 — Visual verification (Playwright screenshots) Take screenshots of affected pages on the live site. Some routes require authentication — use the test credentials to log in first. Test user credentials are available as env vars: TEST_USER_EMAIL, TEST_USER_PASSWORD Determine which routes to screenshot by reading the PR diff. Map changed files to routes: - `src/app/(auth)/sign-in/*` → `/sign-in` - `src/app/(auth)/sign-up/*` → `/sign-up` - `src/app/(app)/[workspaceSlug]/page.tsx` → `/{workspaceSlug}` (authenticated) - `src/app/(app)/[workspaceSlug]/[pageId]/*` → `/{workspaceSlug}/{pageId}` (authenticated) - `src/app/(app)/[workspaceSlug]/settings/*` → `/{workspaceSlug}/settings` (authenticated) - `src/components/editor/*` → `/{workspaceSlug}/{pageId}` (authenticated — editor is rendered here) - `src/app/page.tsx` → `/` (public landing page) Classify each route as **public** or **authenticated**. ```js import { chromium } from 'playwright'; const BASE = 'https://memo.software-factory.dev'; const EMAIL = process.env.TEST_USER_EMAIL; const PASSWORD = process.env.TEST_USER_PASSWORD; const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); // --- Authentication helper --- // Returns an authenticated browser context with session cookies. // Reuse this context for all authenticated route screenshots. async function createAuthenticatedContext() { const ctx = await browser.newContext(); const page = await ctx.newPage(); await page.goto(BASE + '/sign-in', { waitUntil: 'networkidle', timeout: 15000 }); await page.fill('input[type="email"]', EMAIL); await page.fill('input[type="password"]', PASSWORD); await page.click('button[type="submit"]'); // Wait for redirect after successful login await page.waitForURL(url => !url.pathname.includes('/sign-in'), { timeout: 15000 }); await page.close(); return ctx; } // --- Discover a valid workspace and page for authenticated routes --- // After login, the app redirects to /{workspaceSlug}. The sidebar uses //