Files
pbakaus_impeccable/tests/inline-ignores.test.mjs
T
Paul Bakaus 144cee5c36 Fix detector coverage for generated UI tells
Remove provider gating, share grid-background detection across source and rendered scan paths, and update the detector catalog and tests.\n\nAI-assisted: prepared by Codex at Paul's request.
2026-07-18 14:21:06 -07:00

211 lines
9.2 KiB
JavaScript

import { describe, test, expect } from 'bun:test';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
parseInlineIgnores,
applyInlineIgnores,
isInlineIgnored,
} from '../cli/engine/shared/inline-ignores.mjs';
import { detectText, detectHtml } from '../cli/engine/detect-antipatterns.mjs';
const CLI = path.resolve('cli/bin/cli.js');
function rules(finding) {
return finding.antipattern;
}
describe('parseInlineIgnores', () => {
test('whole-file directive collects rules', () => {
const d = parseInlineIgnores('/* impeccable-disable overused-font, bounce-easing */');
expect([...d.file].sort()).toEqual(['bounce-easing', 'overused-font']);
expect(d.line.size).toBe(0);
expect(d.nextLine.size).toBe(0);
});
test('bare directive and explicit * both mean every rule', () => {
expect([...parseInlineIgnores('// impeccable-disable').file]).toEqual(['*']);
expect([...parseInlineIgnores('// impeccable-disable *').file]).toEqual(['*']);
});
test('disable-line targets its own line, disable-next-line targets the line below', () => {
const content = [
'a', // line 1
'b /* impeccable-disable-line overused-font */', // line 2
'// impeccable-disable-next-line side-tab', // line 3 -> targets line 4
'd', // line 4
].join('\n');
const d = parseInlineIgnores(content);
expect([...d.line.get(2)]).toEqual(['overused-font']);
expect([...d.nextLine.get(4)]).toEqual(['side-tab']);
});
test('strips eslint -- and biome : reasons from the rule list', () => {
expect([...parseInlineIgnores('// impeccable-disable overused-font -- brand font, exported doc').file])
.toEqual(['overused-font']);
expect([...parseInlineIgnores('# impeccable-disable bounce-easing: intentional bounce').file])
.toEqual(['bounce-easing']);
});
test('strips trailing comment closers across syntaxes', () => {
expect([...parseInlineIgnores('<!-- impeccable-disable overused-font -->').file]).toEqual(['overused-font']);
expect([...parseInlineIgnores('{/* impeccable-disable overused-font */}').file]).toEqual(['overused-font']);
expect([...parseInlineIgnores('{# impeccable-disable overused-font #}').file]).toEqual(['overused-font']);
});
test('directive keyword is case-insensitive (fast-path matches the regex)', () => {
expect([...parseInlineIgnores('// Impeccable-Disable overused-font').file]).toEqual(['overused-font']);
expect([...parseInlineIgnores('/* IMPECCABLE-DISABLE-LINE side-tab */').line.get(1)]).toEqual(['side-tab']);
});
test('no directive present is a cheap no-op', () => {
const d = parseInlineIgnores('.a { color: red }');
expect(d.file.size).toBe(0);
expect(d.line.size).toBe(0);
expect(d.nextLine.size).toBe(0);
});
});
describe('applyInlineIgnores / isInlineIgnored', () => {
const findings = [
{ antipattern: 'overused-font', line: 5 },
{ antipattern: 'side-tab', line: 5 },
{ antipattern: 'overused-font', line: 0 }, // no line (static-HTML shape)
];
test('whole-file directive drops every matching finding regardless of line', () => {
const out = applyInlineIgnores(findings, '/* impeccable-disable overused-font */');
expect(out.map(rules)).toEqual(['side-tab']);
});
test('* drops everything', () => {
expect(applyInlineIgnores(findings, '// impeccable-disable *')).toEqual([]);
});
test('line-scoped directive only affects the matching line and rule', () => {
const content = ['', '', '', '', 'x /* impeccable-disable-line overused-font */'].join('\n');
const out = applyInlineIgnores(findings, content);
// the line-5 overused-font goes; side-tab on line 5 and the line-less one stay
expect(out.map(rules).sort()).toEqual(['overused-font', 'side-tab']);
expect(out.some((f) => f.antipattern === 'overused-font' && f.line === 5)).toBe(false);
});
test('returns the input untouched when there are no directives', () => {
const out = applyInlineIgnores(findings, '.a {}');
expect(out).toBe(findings);
});
test('isInlineIgnored never matches a line-scoped directive for a line-less finding', () => {
const d = parseInlineIgnores('x /* impeccable-disable-line overused-font */');
expect(isInlineIgnored({ antipattern: 'overused-font', line: 0 }, d)).toBe(false);
});
});
describe('detectText honors inline directives', () => {
const opts = {};
test('disable-line suppresses a same-line finding', () => {
const flagged = detectText('.a { font-family: Inter; }', 'a.css', opts);
expect(flagged.some((f) => f.antipattern === 'overused-font')).toBe(true);
const waived = detectText('.a { font-family: Inter; } /* impeccable-disable-line overused-font */', 'a.css', opts);
expect(waived.some((f) => f.antipattern === 'overused-font')).toBe(false);
});
test('disable-next-line suppresses the finding on the following line', () => {
const content = '/* impeccable-disable-next-line overused-font */\n.a { font-family: Inter; }';
const waived = detectText(content, 'a.css', opts);
expect(waived.some((f) => f.antipattern === 'overused-font')).toBe(false);
});
test('whole-file directive suppresses regardless of where the finding is', () => {
const content = '/* impeccable-disable overused-font */\n.a {}\n.b { font-family: Inter; }';
const waived = detectText(content, 'a.css', opts);
expect(waived.some((f) => f.antipattern === 'overused-font')).toBe(false);
});
test('inlineIgnores:false bypasses the directive', () => {
const content = '.a { font-family: Inter; } /* impeccable-disable-line overused-font */';
const raw = detectText(content, 'a.css', { inlineIgnores: false });
expect(raw.some((f) => f.antipattern === 'overused-font')).toBe(true);
});
test('line keys align with detector line numbers on CRLF endings', () => {
// detectText numbers lines with split('\n'); parseInlineIgnores must match.
const content = '.a { font-family: Inter; }\r\n.b { font-family: Roboto; } /* impeccable-disable-line overused-font */';
const out = detectText(content, 'a.css', opts);
const fonts = out.filter((f) => f.antipattern === 'overused-font').map((f) => f.line);
expect(fonts).toEqual([1]); // Inter on line 1 stays; Roboto on line 2 is waived
});
test('a directive for one rule leaves other findings intact', () => {
const content = '.a { font-family: Inter; } /* impeccable-disable-line side-tab */';
const out = detectText(content, 'a.css', opts);
expect(out.some((f) => f.antipattern === 'overused-font')).toBe(true);
});
});
describe('detectHtml honors whole-file directives (line-less findings)', () => {
const page = (extra = '') => `<!DOCTYPE html><html><head>${extra}
<style>body { font-family: Inter, sans-serif; }</style></head>
<body><p>Some real paragraph text here for the typography pass.</p>
<h1>Heading</h1><h2>Sub</h2></body></html>`;
test('overused-font fires without a directive', async () => {
const flagged = await detectHtml(await writeTmp(page()));
expect(flagged.some((f) => f.antipattern === 'overused-font')).toBe(true);
});
test('whole-file directive in an HTML comment suppresses it', async () => {
const file = await writeTmp(page('<!-- impeccable-disable overused-font -- exported brand doc -->'));
const waived = await detectHtml(file);
expect(waived.some((f) => f.antipattern === 'overused-font')).toBe(false);
});
test('inlineIgnores:false bypasses it', async () => {
const file = await writeTmp(page('<!-- impeccable-disable overused-font -->'));
const raw = await detectHtml(file, { inlineIgnores: false });
expect(raw.some((f) => f.antipattern === 'overused-font')).toBe(true);
});
});
describe('detect CLI end-to-end', () => {
function run(args) {
return spawnSync(process.execPath, [CLI, 'detect', ...args], { encoding: 'utf-8' });
}
test('inline directive is honored by default, --no-inline-ignores and --no-config bypass it', async () => {
const file = await writeTmp(
'<!DOCTYPE html><html><head><!-- impeccable-disable overused-font -->\n' +
'<style>body { font-family: Inter, sans-serif; }</style></head>\n' +
'<body><p>Paragraph copy for the typography analyzer to read.</p><h1>H</h1><h2>S</h2></body></html>',
'.html',
);
const honored = run([file, '--json', '--no-design-system']);
expect(JSON.parse(honored.stdout).some((f) => f.antipattern === 'overused-font')).toBe(false);
const bypassed = run([file, '--json', '--no-design-system', '--no-inline-ignores']);
expect(JSON.parse(bypassed.stdout).some((f) => f.antipattern === 'overused-font')).toBe(true);
const rawConfig = run([file, '--json', '--no-config']);
expect(JSON.parse(rawConfig.stdout).some((f) => f.antipattern === 'overused-font')).toBe(true);
});
});
let tmpDir;
async function writeTmp(content, ext = '.html') {
if (!tmpDir) tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-inline-'));
const file = path.join(tmpDir, `f${Math.abs(hash(content))}${ext}`);
fs.writeFileSync(file, content);
return file;
}
function hash(str) {
let h = 0;
for (let i = 0; i < str.length; i++) h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
return h;
}