--- import fs from 'node:fs'; import path from 'node:path'; import Base from '../../layouts/Base.astro'; import '../../styles/sub-pages.css'; import '../../styles/detector-lab.css'; import { createDetectorProfile, detectHtml, summarizeDetectorProfile, } from '../../../cli/engine/detect-antipatterns.mjs'; const ROOT = process.cwd(); const FIXTURES_DIR = path.join(ROOT, 'tests', 'fixtures', 'antipatterns'); const FIXTURES = [ { file: 'border-baseline.html', group: 'Borders', title: 'Border side tabs', summary: 'Side-by-side positive and negative cases for colored side borders and rounded accent borders.', }, { file: 'partial-component.html', group: 'Baseline', title: 'Partial component', summary: 'Component-like HTML where page-level rules must stay quiet.', }, { file: 'linked-stylesheet.html', group: 'Static CSS', title: 'Linked stylesheet', summary: 'Local CSS inlining, selector matching, and cascade coverage.', }, { file: 'modern-color-borders.html', group: 'Borders', title: 'Modern color borders', summary: 'OKLCH, LAB, LCH, HSL, HWB, and custom-property border cases.', }, { file: 'color.html', group: 'Color', title: 'Color and contrast', summary: 'Solid contrast, gray-on-color, gradient stops, links, buttons, and emoji skips.', }, { file: 'visual-contrast.html', group: 'Color', title: 'Visual contrast fallback', summary: 'Image-background text cases for the Puppeteer pixel-diff contrast pass.', }, { file: 'legitimate-borders.html', group: 'Borders', title: 'Legitimate borders', summary: 'Border patterns that should pass despite being visually close to side tabs.', }, { file: 'typography.html', group: 'Typography', title: 'Typography side by side', summary: 'Element-level typography checks with visible flag/pass highlights.', }, { file: 'quality.html', group: 'Typography', title: 'Quality and readability', summary: 'Text quality checks, plus browser-only line length in the live pane.', }, { file: 'italic-serif-display.html', group: 'Typography', title: 'Italic serif display', summary: 'Large decorative serif italics versus legitimate emphasis.', }, { file: 'hero-eyebrow-chip.html', group: 'Typography', title: 'Hero eyebrow chip', summary: 'Uppercase micro-labels and pills above hero headings.', }, { file: 'layout.html', group: 'Layout', title: 'Layout', summary: 'Nested cards and page-level layout patterns.', }, { file: 'repeated-section-kickers.html', group: 'Layout', title: 'Repeated section kickers', summary: 'Repeated scaffolding labels that create AI-page rhythm.', }, { file: 'icon-tile-stack.html', group: 'Layout', title: 'Icon tile stack', summary: 'Large icon tiles stacked over headings.', }, { file: 'cramped-padding.html', group: 'Browser layout', title: 'Cramped padding', summary: 'Browser-only padding checks that need real element rectangles.', }, { file: 'body-text-viewport-edge.html', group: 'Browser layout', title: 'Viewport-edge body text', summary: 'Browser-only body text that bleeds to the viewport edge.', }, { file: 'motion.html', group: 'Motion', title: 'Motion', summary: 'Bounce easing and layout-property transitions.', }, { file: 'glow.html', group: 'Motion', title: 'Dark glow', summary: 'Chromatic shadows on dark surfaces versus safe shadows.', }, { file: 'overlay-positioning.html', group: 'Overlay', title: 'Overlay positioning', summary: 'Edge cases for the browser overlay itself.', }, ]; function nowMs() { return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); } function roundMs(value: number) { return Number(value.toFixed(3)); } function displayMs(value: number) { if (!Number.isFinite(value)) return '0 ms'; if (value >= 1000) return `${(value / 1000).toFixed(2)} s`; if (value < 10) return `${value.toFixed(2)} ms`; return `${value.toFixed(1)} ms`; } function slug(file: string) { return file.replace(/\.html$/, ''); } function routeFor(file: string) { return `/detector/fixtures/antipatterns/${file}`; } function countByRule(findings: Array<{ antipattern: string }>) { const counts = new Map(); for (const finding of findings) { counts.set(finding.antipattern, (counts.get(finding.antipattern) || 0) + 1); } return [...counts.entries()] .map(([ruleId, count]) => ({ ruleId, count })) .sort((a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId)); } async function measureFixture(meta: typeof FIXTURES[number]) { const filePath = path.join(FIXTURES_DIR, meta.file); const profile = createDetectorProfile(); const started = nowMs(); const findings = await detectHtml(filePath, { profile }); const totalMs = roundMs(nowMs() - started); return { ...meta, id: slug(meta.file), url: routeFor(meta.file), bytes: fs.statSync(filePath).size, staticTotalMs: totalMs, staticFindings: findings.length, findingsByRule: countByRule(findings), profile: summarizeDetectorProfile(profile) .map(row => ({ engine: row.engine, phase: row.phase, ruleId: row.ruleId, calls: row.calls, totalMs: row.totalMs, avgMs: row.avgMs, p50: row.p50, p95: row.p95, findings: row.findings, })), }; } function percentile(values: number[], pct: number) { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((pct / 100) * sorted.length) - 1)); return sorted[idx]; } function aggregateProfile(fixtures: Awaited>[]) { const groups = new Map(); for (const fixture of fixtures) { for (const row of fixture.profile) { const key = `${row.engine}\u0000${row.phase}\u0000${row.ruleId}`; let group = groups.get(key); if (!group) { group = { engine: row.engine, phase: row.phase, ruleId: row.ruleId, calls: 0, totalMs: 0, findings: 0, samples: [], }; groups.set(key, group); } group.calls += row.calls; group.totalMs += row.totalMs; group.findings += row.findings; group.samples.push(row.p50); } } return [...groups.values()] .map(group => ({ engine: group.engine, phase: group.phase, ruleId: group.ruleId, calls: group.calls, totalMs: roundMs(group.totalMs), avgMs: roundMs(group.totalMs / Math.max(1, group.calls)), p50: roundMs(percentile(group.samples, 50)), p95: roundMs(percentile(group.samples, 95)), findings: group.findings, })) .sort((a, b) => b.totalMs - a.totalMs); } await detectHtml(path.join(FIXTURES_DIR, 'should-pass.html'), { profile: createDetectorProfile() }); const fixtureData = []; for (const fixture of FIXTURES) { fixtureData.push(await measureFixture(fixture)); } const globalProfile = aggregateProfile(fixtureData); const totalStaticMs = roundMs(fixtureData.reduce((sum, item) => sum + item.staticTotalMs, 0)); const totalFindings = fixtureData.reduce((sum, item) => sum + item.staticFindings, 0); const slowestRow = globalProfile[0]; const groups = [...new Set(fixtureData.map(item => item.group))]; const dashboardData = { generatedAt: new Date().toISOString(), fixtures: fixtureData, globalProfile, }; const dashboardJson = JSON.stringify(dashboardData).replace(/

{fixtureData[0].group}

{fixtureData[0].title}

{fixtureData[0].summary}

Open fixture

Per fixture

{fixtureData[0].title} timings

Rows come directly from the profiling API.

Phase Check Calls Total P95 Findings

All fixtures

Slowest checks

Aggregated across every fixture in this lab.

{globalProfile.slice(0, 24).map(row => ( ))}
Engine Phase Check Calls Total P95 Findings
{row.engine} {row.phase} {row.ruleId} {row.calls} {row.totalMs} ms {row.p95} ms {row.findings}