/** * Static HTML/CSS fixture tests for anti-pattern detection. * Run via Node's built-in test runner (not bun). * * Usage: node --test tests/detect-antipatterns-fixtures.test.mjs */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import path from 'path'; import { fileURLToPath } from 'url'; import { detectHtml, detectText, normalizeDesignSystem, } from '../cli/engine/detect-antipatterns.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURES = path.join(__dirname, 'fixtures', 'antipatterns'); describe('detectHtml — static HTML/CSS fixtures', () => { it('should-flag: catches border anti-patterns', async () => { const f = await detectHtml(path.join(FIXTURES, 'should-flag.html')); assert.ok(f.some(r => r.antipattern === 'side-tab')); assert.ok(f.some(r => r.antipattern === 'border-accent-on-rounded')); }); it('should-pass: zero border findings', async () => { const f = await detectHtml(path.join(FIXTURES, 'should-pass.html')); assert.equal(f.filter(r => r.antipattern === 'side-tab' || r.antipattern === 'border-accent-on-rounded').length, 0); }); it('border-baseline: paired side-tab fixture flags only the positive column', async () => { const f = await detectHtml(path.join(FIXTURES, 'border-baseline.html')); const sideTabs = f.filter(r => r.antipattern === 'side-tab'); const accents = f.filter(r => r.antipattern === 'border-accent-on-rounded'); assert.equal( sideTabs.length, 4, `expected 4 side-tab findings, got ${sideTabs.length}: ${sideTabs.map(r => r.snippet).join('; ')}` ); assert.equal( accents.length, 2, `expected 2 rounded accent findings, got ${accents.length}: ${accents.map(r => r.snippet).join('; ')}` ); }); it('linked-stylesheet: catches borders, no false positives', async () => { const f = await detectHtml(path.join(FIXTURES, 'linked-stylesheet.html')); assert.ok(f.some(r => r.antipattern === 'side-tab')); assert.ok(f.some(r => r.antipattern === 'border-accent-on-rounded')); assert.equal(f.filter(r => r.snippet?.includes('clean')).length, 0); assert.equal( f.filter(r => r.antipattern !== 'side-tab' && r.antipattern !== 'border-accent-on-rounded').length, 0, `expected only border findings, got: ${f.map(r => `${r.antipattern}:${r.snippet}`).join('; ')}` ); }); it('partial-component: flags borders, skips page-level', async () => { const f = await detectHtml(path.join(FIXTURES, 'partial-component.html')); assert.ok(f.some(r => r.antipattern === 'side-tab')); assert.equal(f.filter(r => r.antipattern === 'flat-type-hierarchy').length, 0); }); it('color: flag column triggers all color rules, pass column adds none', async () => { const f = await detectHtml(path.join(FIXTURES, 'color.html')); // pure-black-white was removed from the skill in v3.2; only the remaining rules // are expected to fire from the flag column. assert.ok(f.some(r => r.antipattern === 'gray-on-color'), 'expected gray-on-color'); assert.ok(f.some(r => r.antipattern === 'low-contrast'), 'expected low-contrast'); assert.ok(f.some(r => r.antipattern === 'gradient-text'), 'expected gradient-text'); assert.ok(f.some(r => r.antipattern === 'ai-color-palette'), 'expected ai-color-palette'); assert.equal( f.some(r => r.antipattern === 'pure-black-white'), false, 'pure-black-white detector was removed in v3.2', ); // Gradient-bg + gray text case (added with the gradient-fix patch) assert.ok( f.some(r => r.antipattern === 'low-contrast' && /#808080|#3b82f6|#8b5cf6/i.test(r.snippet || '')), 'expected low-contrast finding for gray heading on blue/purple gradient', ); assert.ok( f.some(r => r.antipattern === 'gray-on-color' && /gradient/i.test(r.snippet || '')), 'expected gray-on-color finding referencing gradient', ); }); it('color: white text on background-image url() ancestor is not flagged as low-contrast', async () => { const f = await detectHtml(path.join(FIXTURES, 'color.html')); // The pass column has white text on a div with background-image: url(). // The detector can't know the image color, so it must not assume the body // bg and report a false low-contrast finding (#ffffff on #fafafa). const falsePositive = f.filter(r => r.antipattern === 'low-contrast' && /#ffffff on #fafafa/i.test(r.snippet || '') ); assert.equal( falsePositive.length, 0, `expected no low-contrast from bg-image ancestor, got: ${falsePositive.map(r => r.snippet).join('; ')}` ); }); it('color: styled and