/**
* 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