mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
* Fix: stop gray-on-color false positives on Tailwind opacity and JSX (#633) Do not treat bg-*/10 tints as solid fills, and pair gray text with chromatic backgrounds only inside the same tag and ternary arm. AI assistance: prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: keep nested ternary arms and post-ternary classes exclusive (#633) Recurse exclusive class scopes so nested else-arms do not pair, and treat classes after a finished ternary as shared across both arms. AI assistance: prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: ignore nullish coalescing when scoping gray-on-color (#633) The second ? in ?? was treated as a ternary delimiter, so exclusive arms stayed in one scope. Prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -223,6 +223,73 @@ describe('detectText — Tailwind side-tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectText — gray-on-color (issue #633)', () => {
|
||||
const grayOnColor = (src, file = 'Repro.jsx') =>
|
||||
detectText(src, file).filter(r => r.antipattern === 'gray-on-color');
|
||||
|
||||
test('opacity hover — no finding', () => {
|
||||
expect(grayOnColor('<button className="text-slate-300 hover:bg-red-500/10 hover:text-red-400">Log out</button>')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('opacity rest — no finding', () => {
|
||||
expect(grayOnColor('<button className="text-slate-300 bg-red-500/10">Log out</button>')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('solid still flags', () => {
|
||||
const f = grayOnColor('<button className="text-slate-300 bg-red-500">Log out</button>');
|
||||
expect(f).toHaveLength(1);
|
||||
expect(f[0].snippet).toContain('text-slate-300 on bg-red-500');
|
||||
});
|
||||
|
||||
test('ternary arms — no finding', () => {
|
||||
expect(grayOnColor(
|
||||
'<button className={`px-4 py-2 text-sm rounded-lg transition-colors ${mode === "a" ? "bg-amber-600 text-white" : "bg-white/5 text-slate-400 hover:bg-white/10"}`}>Mode</button>',
|
||||
)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('ternary with comparison > does not close the tag early', () => {
|
||||
expect(grayOnColor(
|
||||
'<button className={mode > 0 ? "bg-amber-600 text-white" : "text-slate-400"}>Mode</button>',
|
||||
)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('siblings on one line — no finding', () => {
|
||||
expect(grayOnColor(
|
||||
'<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded bg-amber-500" /><span className="text-slate-400">Vital few</span></div>',
|
||||
)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('cn() simultaneous args still flag', () => {
|
||||
const f = grayOnColor('<button className={cn("text-slate-400", "bg-blue-600")}>Go</button>');
|
||||
expect(f).toHaveLength(1);
|
||||
expect(f[0].snippet).toContain('text-slate-400 on bg-blue-600');
|
||||
});
|
||||
|
||||
test('cn() + ternary with gray in common prefix still flags', () => {
|
||||
const f = grayOnColor('<button className={cn("text-slate-400", mode === "a" ? "bg-amber-600" : "bg-white")}>Go</button>');
|
||||
expect(f).toHaveLength(1);
|
||||
expect(f[0].snippet).toContain('text-slate-400 on bg-amber-600');
|
||||
});
|
||||
|
||||
test('nested exclusive ternary arms — no finding', () => {
|
||||
expect(grayOnColor(
|
||||
'<div className={a ? "bg-red-500" : b ? "text-slate-400" : "bg-blue-600"} />',
|
||||
)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('shared classes after a ternary still flag', () => {
|
||||
const f = grayOnColor('<div className={cn(a ? "bg-red-500" : "bg-blue-600", "text-slate-400")} />');
|
||||
expect(f).toHaveLength(1);
|
||||
expect(f[0].snippet).toContain('text-slate-400 on bg-red-500');
|
||||
});
|
||||
|
||||
test('nullish coalescing before a ternary — no finding', () => {
|
||||
expect(grayOnColor(
|
||||
'<div className={value ?? fallback ? "bg-red-500" : "text-slate-400"} />',
|
||||
)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectText — broken images in source comments', () => {
|
||||
test('ignores img tags mentioned in JavaScript comments', () => {
|
||||
const source = [
|
||||
@@ -1809,6 +1876,38 @@ describe('checkColors — oklch computed colors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkColors — Tailwind classList gray-on-color (issue #633)', () => {
|
||||
const grayOnColor = (classList) =>
|
||||
checkColors({
|
||||
tag: 'div',
|
||||
textColor: null,
|
||||
bgColor: null,
|
||||
effectiveBg: null,
|
||||
effectiveBgStops: null,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
hasDirectText: true,
|
||||
isEmojiOnly: false,
|
||||
bgClip: '',
|
||||
bgImage: '',
|
||||
classList,
|
||||
}).filter(r => r.id === 'gray-on-color');
|
||||
|
||||
test('opacity hover — no finding', () => {
|
||||
expect(grayOnColor('text-slate-300 hover:bg-red-500/10')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('opacity rest — no finding', () => {
|
||||
expect(grayOnColor('text-slate-300 bg-red-500/10')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('solid still flags', () => {
|
||||
const f = grayOnColor('text-slate-300 bg-red-500');
|
||||
expect(f).toHaveLength(1);
|
||||
expect(f[0].snippet).toBe('text-slate-300 on bg-red-500');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Numbered section labels — pure helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user