import { describe, test, expect } from 'bun:test'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { spawnSync } from 'child_process'; import { ANTIPATTERNS, checkElementBorders, checkElementMotion, checkElementGlow, isNeutralColor, isFullPage, detectText, detectHtml, extractStyleBlocks, extractCSSinJS, walkDir, SCANNABLE_EXTENSIONS, buildImportGraph, resolveImport, detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS, } from '../cli/engine/detect-antipatterns.mjs'; const FIXTURES = path.join(import.meta.dir, 'fixtures', 'antipatterns'); const SCRIPT = path.join(import.meta.dir, '..', 'cli', 'engine', 'detect-antipatterns.mjs'); const BENCH_SCRIPT = path.join(import.meta.dir, '..', 'scripts', 'benchmark-detector.mjs'); function writeStaticFixture(files) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-static-')); for (const [name, contents] of Object.entries(files)) { const fullPath = path.join(dir, name); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, contents); } return { dir, file: path.join(dir, 'index.html') }; } async function withStaticFixture(files, callback) { const fixture = writeStaticFixture(files); try { return await callback(fixture); } finally { fs.rmSync(fixture.dir, { recursive: true, force: true }); } } function findingIds(findings) { return findings.map(f => f.antipattern); } // --------------------------------------------------------------------------- // Core: checkElementBorders (computed style simulation) // --------------------------------------------------------------------------- describe('checkElementBorders', () => { function mockStyle(overrides) { return { borderTopWidth: '0', borderRightWidth: '0', borderBottomWidth: '0', borderLeftWidth: '0', borderTopColor: '', borderRightColor: '', borderBottomColor: '', borderLeftColor: '', borderRadius: '0', ...overrides }; } test('detects side-tab with radius', () => { const f = checkElementBorders('div', mockStyle({ borderLeftWidth: '4', borderLeftColor: 'rgb(59, 130, 246)', borderRadius: '12', })); expect(f.length).toBe(1); expect(f[0].id).toBe('side-tab'); }); test('detects side-tab without radius (thick)', () => { const f = checkElementBorders('div', mockStyle({ borderLeftWidth: '4', borderLeftColor: 'rgb(59, 130, 246)', })); expect(f.length).toBe(1); expect(f[0].id).toBe('side-tab'); }); test('skips side border below threshold without radius', () => { const f = checkElementBorders('div', mockStyle({ borderLeftWidth: '2', borderLeftColor: 'rgb(59, 130, 246)', })); expect(f).toHaveLength(0); }); test('detects border-accent-on-rounded (top)', () => { const f = checkElementBorders('div', mockStyle({ borderTopWidth: '3', borderTopColor: 'rgb(139, 92, 246)', borderRadius: '12', })); expect(f.length).toBe(1); expect(f[0].id).toBe('border-accent-on-rounded'); }); test('skips safe tags', () => { const f = checkElementBorders('blockquote', mockStyle({ borderLeftWidth: '4', borderLeftColor: 'rgb(59, 130, 246)', })); expect(f).toHaveLength(0); }); test('skips neutral colors', () => { const f = checkElementBorders('div', mockStyle({ borderLeftWidth: '4', borderLeftColor: 'rgb(200, 200, 200)', })); expect(f).toHaveLength(0); }); test('skips uniform borders (not accent)', () => { const f = checkElementBorders('div', mockStyle({ borderTopWidth: '2', borderRightWidth: '2', borderBottomWidth: '2', borderLeftWidth: '2', borderTopColor: 'rgb(59, 130, 246)', borderRightColor: 'rgb(59, 130, 246)', borderBottomColor: 'rgb(59, 130, 246)', borderLeftColor: 'rgb(59, 130, 246)', })); expect(f).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // isNeutralColor // --------------------------------------------------------------------------- describe('isNeutralColor', () => { test('gray is neutral', () => expect(isNeutralColor('rgb(200, 200, 200)')).toBe(true)); test('blue is not neutral', () => expect(isNeutralColor('rgb(59, 130, 246)')).toBe(false)); test('transparent is neutral', () => expect(isNeutralColor('transparent')).toBe(true)); test('null is neutral', () => expect(isNeutralColor(null)).toBe(true)); }); // --------------------------------------------------------------------------- // Regex fallback (detectText) // --------------------------------------------------------------------------- describe('detectText — Tailwind side-tab', () => { test('detects border-l-4 (thick, no rounded needed)', () => { const f = detectText('
', 'test.html'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); test('detects border-l-1 + rounded', () => { const f = detectText('
', 'test.html'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); test('ignores border-l-1 without rounded', () => { const f = detectText('
', 'test.html'); expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0); }); test('ignores border-t without rounded', () => { const f = detectText('
', 'test.html'); expect(f.filter(r => r.antipattern === 'border-accent-on-rounded')).toHaveLength(0); }); }); describe('detectText — CSS borders', () => { test('detects border-left shorthand', () => { const f = detectText('.card { border-left: 4px solid #3b82f6; }', 'test.css'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); test('ignores neutral border', () => { const f = detectText('.card { border-left: 4px solid #e5e7eb; }', 'test.css'); expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0); }); test('skips blockquote', () => { const f = detectText('
', 'test.html'); expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0); }); }); describe('detectText — overused fonts', () => { test('detects Inter', () => { const f = detectText("body { font-family: 'Inter', sans-serif; }", 'test.css'); expect(f.some(r => r.antipattern === 'overused-font')).toBe(true); }); test('detects Fraunces (current AI-default monoculture)', () => { const f = detectText("h1 { font-family: 'Fraunces', Georgia, serif; }", 'test.css'); expect(f.some(r => r.antipattern === 'overused-font')).toBe(true); }); test('detects Geist (Vercel-default monoculture)', () => { const f = detectText("body { font-family: 'Geist', sans-serif; }", 'test.css'); expect(f.some(r => r.antipattern === 'overused-font')).toBe(true); }); test('does not flag distinctive fonts', () => { const f = detectText("body { font-family: 'Karla', sans-serif; }", 'test.css'); expect(f.filter(r => r.antipattern === 'overused-font')).toHaveLength(0); }); }); describe('detectText — flat type hierarchy', () => { test('flags sizes too close together', () => { const page = ''; const f = detectText(page, 'test.html'); expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true); }); test('passes good hierarchy', () => { const page = ''; const f = detectText(page, 'test.html'); expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0); }); }); // Static HTML/CSS fixture tests moved to detect-antipatterns-fixtures.test.mjs (run via node --test) // --------------------------------------------------------------------------- // Full page vs partial detection // --------------------------------------------------------------------------- describe('isFullPage', () => { test('detects DOCTYPE', () => expect(isFullPage('')).toBe(true)); test('detects ', () => expect(isFullPage('')).toBe(true)); test('detects ', () => expect(isFullPage('')).toBe(true)); test('rejects component/partial', () => expect(isFullPage('
content
')).toBe(false)); test('rejects JSX', () => expect(isFullPage('export default function Card() { return
hi
}')).toBe(false)); }); describe('partials skip page-level checks', () => { test('regex: partial with flat hierarchy is not flagged', () => { const partial = '
text
\n
text
\n
text
'; const f = detectText(partial, 'card.tsx'); expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0); }); test('regex: partial with single overused font is not flagged for single-font', () => { const partial = `
text
\n`.repeat(25); const f = detectText(partial, 'card.tsx'); expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0); }); test('regex: partial still flags border anti-patterns', () => { const partial = '
card
'; const f = detectText(partial, 'card.tsx'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); test('regex: full page with flat hierarchy IS flagged', () => { const page = '\n' + '

h1

\n

h2

\n' + '

p

\ns\n' + 'sm\n'; const f = detectText(page, 'index.html'); expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true); }); }); // --------------------------------------------------------------------------- // Layout anti-patterns // --------------------------------------------------------------------------- describe('detectHtml — layout', () => { test('detects monotonous spacing via regex', () => { // A page where every padding/margin is 16px const html = '' + '

a

'.repeat(5) + ''; const f = detectText(html, 'test.html'); expect(f.some(r => r.antipattern === 'monotonous-spacing')).toBe(true); }); }); // --------------------------------------------------------------------------- // Motion anti-patterns // --------------------------------------------------------------------------- describe('checkElementMotion', () => { function mockStyle(overrides) { return { transitionProperty: '', animationName: 'none', animationTimingFunction: '', transitionTimingFunction: '', ...overrides }; } test('detects bounce animation name', () => { const f = checkElementMotion('div', mockStyle({ animationName: 'bounce' })); expect(f.some(r => r.id === 'bounce-easing')).toBe(true); }); test('detects elastic animation name', () => { const f = checkElementMotion('div', mockStyle({ animationName: 'elastic-in' })); expect(f.some(r => r.id === 'bounce-easing')).toBe(true); }); test('detects overshoot cubic-bezier in animation timing', () => { const f = checkElementMotion('div', mockStyle({ animationTimingFunction: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', })); expect(f.some(r => r.id === 'bounce-easing')).toBe(true); }); test('detects overshoot cubic-bezier in transition timing', () => { const f = checkElementMotion('div', mockStyle({ transitionTimingFunction: 'cubic-bezier(0.34, 1.56, 0.64, 1)', })); expect(f.some(r => r.id === 'bounce-easing')).toBe(true); }); test('passes standard ease-out-quart', () => { const f = checkElementMotion('div', mockStyle({ transitionTimingFunction: 'cubic-bezier(0.25, 1, 0.5, 1)', })); expect(f.filter(r => r.id === 'bounce-easing')).toHaveLength(0); }); test('passes standard ease', () => { const f = checkElementMotion('div', mockStyle({ transitionTimingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)', })); expect(f.filter(r => r.id === 'bounce-easing')).toHaveLength(0); }); test('detects width transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'width' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('detects height transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'height' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('detects padding transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'padding' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('detects margin transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'margin' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('detects max-height transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'max-height' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('detects layout prop among mixed transitions', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'opacity, width, color' })); expect(f.some(r => r.id === 'layout-transition')).toBe(true); }); test('passes transform transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'transform' })); expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0); }); test('passes opacity transition', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'opacity' })); expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0); }); test('skips transition: all', () => { const f = checkElementMotion('div', mockStyle({ transitionProperty: 'all' })); expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0); }); test('skips safe tags', () => { const f = checkElementMotion('button', mockStyle({ animationName: 'bounce', transitionProperty: 'width', })); expect(f).toHaveLength(0); }); }); describe('detectText — motion', () => { test('detects animate-bounce Tailwind class', () => { const f = detectText('
loading
', 'test.html'); expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true); }); test('detects animation: bounce CSS', () => { const f = detectText('.icon { animation: bounce 1s infinite; }', 'test.css'); expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true); }); test('detects animation-name: elastic', () => { const f = detectText('.card { animation-name: elastic; }', 'test.css'); expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true); }); test('detects overshoot cubic-bezier', () => { const f = detectText('.btn { transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }', 'test.css'); expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true); }); test('passes standard cubic-bezier', () => { const f = detectText('.btn { transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1); }', 'test.css'); expect(f.filter(r => r.antipattern === 'bounce-easing')).toHaveLength(0); }); test('detects transition: width', () => { const f = detectText('.sidebar { transition: width 0.3s ease; }', 'test.css'); expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true); }); test('detects transition: height', () => { const f = detectText('.panel { transition: height 0.4s ease-out; }', 'test.css'); expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true); }); test('detects transition: max-height', () => { const f = detectText('.accordion { transition: max-height 0.5s ease; }', 'test.css'); expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true); }); test('detects transition-property: width', () => { const f = detectText('.box { transition-property: width; transition-duration: 0.3s; }', 'test.css'); expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true); }); test('skips transition: all', () => { const f = detectText('.card { transition: all 0.3s ease; }', 'test.css'); expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); }); test('skips transition: transform', () => { const f = detectText('.card { transition: transform 0.3s ease; }', 'test.css'); expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); }); test('skips transition: opacity', () => { const f = detectText('.btn { transition: opacity 0.2s ease; }', 'test.css'); expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // Dark glow anti-pattern // --------------------------------------------------------------------------- describe('checkElementGlow', () => { function mockStyle(overrides) { return { boxShadow: 'none', backgroundColor: '', ...overrides }; } // Dark bg = luminance < 0.1 (e.g. #111827 = gray-900) const darkBg = { r: 17, g: 24, b: 39 }; // #111827 const lightBg = { r: 249, g: 250, b: 251 }; // #f9fafb const mediumBg = { r: 107, g: 114, b: 128 }; // #6b7280 test('detects blue glow on dark background', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px', }), darkBg); expect(f.some(r => r.id === 'dark-glow')).toBe(true); }); test('detects purple glow on dark background', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(139, 92, 246, 0.35) 0px 0px 25px 0px', }), darkBg); expect(f.some(r => r.id === 'dark-glow')).toBe(true); }); test('detects glow in multi-shadow', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(0, 0, 0, 0.3) 0px 4px 6px 0px, rgba(168, 85, 247, 0.3) 0px 0px 30px 0px', }), darkBg); expect(f.some(r => r.id === 'dark-glow')).toBe(true); }); test('passes gray shadow on dark background', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(0, 0, 0, 0.4) 0px 4px 12px 0px', }), darkBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('passes colored shadow on light background', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px', }), lightBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('passes colored shadow on medium gray background', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.5) 0px 0px 20px 0px', }), mediumBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('passes focus ring (spread only, no blur)', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.5) 0px 0px 0px 3px', }), darkBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('passes subtle shadow (blur < 5px)', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.2) 0px 1px 3px 0px', }), darkBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('passes no shadow', () => { const f = checkElementGlow('div', mockStyle({ boxShadow: 'none' }), darkBg); expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0); }); test('detects glow on buttons (not skipped by safe tags)', () => { const f = checkElementGlow('button', mockStyle({ boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px', }), darkBg); expect(f.some(r => r.id === 'dark-glow')).toBe(true); }); }); describe('detectText — dark glow', () => { test('detects colored box-shadow glow on dark background', () => { const html = '
glow
'; const f = detectText(html, 'test.html'); expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true); }); test('skips gray shadow on dark background', () => { const html = '
shadow
'; const f = detectText(html, 'test.html'); expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0); }); test('skips colored shadow on light page', () => { const html = '
glow
'; const f = detectText(html, 'test.html'); expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // Static HTML/CSS engine // --------------------------------------------------------------------------- describe('detectHtml — static HTML/CSS engine', () => { test('inlines local linked stylesheets', async () => { const f = await detectHtml(path.join(FIXTURES, 'linked-stylesheet.html')); expect(findingIds(f)).toContain('side-tab'); }); test('flattens @layer, resolves CSS variables and fallbacks, and skips unsupported selectors', async () => { await withStaticFixture({ 'index.html': `
Layer variable side tab
Fallback variable top accent
`, }, async ({ file }) => { const profile = []; const f = await detectHtml(file, { profile }); const ids = findingIds(f); expect(ids).toContain('side-tab'); expect(ids).toContain('border-accent-on-rounded'); expect(profile.some(e => e.engine === 'static-html' && e.ruleId === 'unsupported-selector')).toBe(true); }); }); test('honors specificity, source order, !important, and inline style precedence', async () => { await withStaticFixture({ 'index.html': `
Specificity neutral pass
Source order chromatic flag
Important neutral pass
Inline chromatic flag
`, }, async ({ file }) => { const f = await detectHtml(file); expect(findingIds(f).filter(id => id === 'side-tab')).toHaveLength(2); }); }); test('expands background, border, font, transition, and animation shorthands', async () => { await withStaticFixture({ 'index.html': `

This tiny paragraph is long enough to trigger both the static font shorthand size and line-height checks.

Border shorthand side tab
Motion shorthand easing
`, }, async ({ file }) => { const ids = findingIds(await detectHtml(file)); expect(ids).toContain('tiny-text'); expect(ids).toContain('tight-leading'); expect(ids).toContain('low-contrast'); expect(ids).toContain('side-tab'); expect(ids).toContain('bounce-easing'); expect(ids).toContain('layout-transition'); }); }); }); // --------------------------------------------------------------------------- // ANTIPATTERNS registry // --------------------------------------------------------------------------- describe('ANTIPATTERNS registry', () => { test('has at least 5 entries', () => { expect(ANTIPATTERNS.length).toBeGreaterThanOrEqual(5); }); test('each entry has required fields', () => { for (const ap of ANTIPATTERNS) { expect(ap.id).toBeTypeOf('string'); expect(ap.name).toBeTypeOf('string'); expect(ap.description).toBeTypeOf('string'); } }); }); // --------------------------------------------------------------------------- // walkDir // --------------------------------------------------------------------------- describe('walkDir', () => { test('finds scannable files', () => { const files = walkDir(FIXTURES); expect(files.length).toBeGreaterThanOrEqual(3); expect(files.every(f => SCANNABLE_EXTENSIONS.has(path.extname(f)))).toBe(true); }); test('returns empty for nonexistent dir', () => { expect(walkDir('/nonexistent/path/12345')).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // CLI integration // --------------------------------------------------------------------------- describe('CLI', () => { function run(...args) { const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 }); return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status }; } test('--help exits 0', () => { const { stdout, code } = run('--help'); expect(code).toBe(0); expect(stdout).toContain('Usage:'); }); test('detect subcommand is not treated as a scan target', () => { const { stderr, code } = run('detect', '--json', path.join(FIXTURES, 'should-pass.html')); expect(code).toBe(0); expect(stderr).not.toContain('cannot access detect'); }); test('should-pass exits 0', () => { const { code } = run(path.join(FIXTURES, 'should-pass.html')); expect(code).toBe(0); }); test('should-flag exits 2 with findings', () => { const { code, stderr } = run(path.join(FIXTURES, 'should-flag.html')); expect(code).toBe(2); expect(stderr).toContain('side-tab'); }); test('--json outputs valid JSON', () => { const { stdout, code } = run('--json', path.join(FIXTURES, 'should-flag.html')); expect(code).toBe(2); const parsed = JSON.parse(stdout.trim()); expect(parsed).toBeArray(); expect(parsed.length).toBeGreaterThan(0); }); test('-json alias outputs valid JSON', () => { const { stdout, stderr, code } = run('-json', path.join(FIXTURES, 'should-flag.html')); expect(code).toBe(2); expect(stderr).not.toContain('cannot access -json'); const parsed = JSON.parse(stdout.trim()); expect(parsed).toBeArray(); expect(parsed.length).toBeGreaterThan(0); }); test('--json on clean file outputs empty array', () => { const { stdout, code } = run('--json', path.join(FIXTURES, 'should-pass.html')); expect(code).toBe(0); expect(JSON.parse(stdout.trim())).toEqual([]); }); test('--fast is accepted but deprecated (no-op, full scan still runs)', () => { const { code, stderr } = run('--fast', path.join(FIXTURES, 'should-flag.html')); expect(code).toBe(2); // still flags the planted anti-patterns via the full scan expect(stderr).toContain('--fast is deprecated'); }); test('linked stylesheet detected (static HTML/CSS default)', () => { const { code, stderr } = run(path.join(FIXTURES, 'linked-stylesheet.html')); expect(code).toBe(2); expect(stderr).toContain('side-tab'); }); test('warns on nonexistent path', () => { const { stderr } = run('/nonexistent/file/xyz.html'); expect(stderr).toContain('Warning'); }); }); // --------------------------------------------------------------------------- // Detector benchmark smoke test // --------------------------------------------------------------------------- describe('benchmark-detector', () => { test('--quick --json emits timing schema', () => { const result = spawnSync('node', [BENCH_SCRIPT, '--quick', '--json'], { encoding: 'utf-8', timeout: 30000, }); expect(result.status).toBe(0); const parsed = JSON.parse(result.stdout.trim()); expect(parsed.version).toBe(1); expect(parsed.quick).toBe(true); expect(parsed.browser).toBe(false); expect(parsed.cases).toBeArray(); expect(parsed.cases.length).toBeGreaterThan(0); expect(parsed.summary).toBeArray(); expect(parsed.summary.length).toBeGreaterThan(0); const okCase = parsed.cases.find(c => c.status === 'ok'); expect(okCase).toBeTruthy(); expect(okCase).toHaveProperty('totalMs'); expect(okCase).toHaveProperty('findings'); expect(okCase.profile).toBeArray(); const row = parsed.summary[0]; for (const key of ['engine', 'phase', 'ruleId', 'target', 'calls', 'totalMs', 'avgMs', 'p50', 'p95', 'findings']) { expect(row).toHaveProperty(key); } }); }); // --------------------------------------------------------------------------- // Tier 1: Vue/Svelte `; const blocks = extractStyleBlocks(vue, '.vue'); expect(blocks.length).toBe(1); expect(blocks[0].content).toContain('border-left: 4px solid blue'); expect(blocks[0].startLine).toBeGreaterThan(1); }); test('extracts multiple `; const blocks = extractStyleBlocks(vue, '.vue'); expect(blocks.length).toBe(2); }); test('extracts `; const blocks = extractStyleBlocks(svelte, '.svelte'); expect(blocks.length).toBe(1); expect(blocks[0].content).toContain('border-right: 4px solid'); }); test('returns empty for non-Vue/Svelte files', () => { const jsx = 'export function Card() { return
hi
; }'; expect(extractStyleBlocks(jsx, '.jsx')).toHaveLength(0); expect(extractStyleBlocks(jsx, '.tsx')).toHaveLength(0); }); test('returns empty when no `; const f = detectText(vue, 'Card.vue'); expect(f.some(r => r.antipattern === 'side-tab')).toBe(true); }); test('detects overused font in `; const f = detectText(vue, 'App.vue'); expect(f.some(r => r.antipattern === 'overused-font')).toBe(true); }); test('detects bounce animation in `; const f = detectText(vue, 'Card.vue'); expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true); }); test('detects gradient-text in `; const f = detectText(vue, 'Hero.vue'); expect(f.some(r => r.antipattern === 'gradient-text')).toBe(true); }); test('detects Tailwind anti-patterns in