mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
Detect side-tab borders on modern color formats and label cards
- Fix isNeutralColor to handle oklch, oklab, lch, lab, hsl, and hwb with format-specific chroma/saturation thresholds. jsdom returns these formats literally, so the previous rgb-only regex caused every modern-format border color to be silently treated as neutral and skipped by checkBorders. - Flip the unknown-format fallback from neutral to colored, so unrecognized color strings err on the side of detection. - Introduce a narrower BORDER_SAFE_TAGS set (SAFE_TAGS minus 'label') used only by the border checks. Card-shaped clickable labels with thick colored side borders are now detected, while colors, motion, and nested-card checks continue to skip labels to avoid false positives on real form labels. - Add tests/fixtures/antipatterns/modern-color-borders.html with 8 flag cases (oklch x3, oklab, lch, lab, plus 2 label cards) and 10 pass cases (neutrals across formats, plain inline labels, thin/neutral-bordered labels, colored-on-all-sides). Reproducer (preop-portal demo): side-tab findings rise from 0 to 12.
This commit is contained in:
@@ -82,6 +82,43 @@ describe('detectHtml — jsdom fixtures', () => {
|
||||
assert.ok(borderFindings.length <= 1);
|
||||
});
|
||||
|
||||
it('modern-color-borders: oklch/oklab/lch/lab side-tabs are flagged, neutrals pass', async () => {
|
||||
// Regression for the isNeutralColor bug where any non-rgb() color format
|
||||
// (oklch, oklab, lch, lab — which jsdom does NOT normalize to rgb) was
|
||||
// misclassified as neutral, causing checkBorders() to silently skip
|
||||
// every element with a modern-color side border.
|
||||
//
|
||||
// Also regression for the SAFE_TAGS/label bug: card-shaped <label>
|
||||
// elements (clickable checklist rows with padding + radius + colored
|
||||
// side border) used to be silently skipped because checkBorders'
|
||||
// SAFE_TAGS gate excluded <label>. The fix narrows that gate so card-
|
||||
// shaped labels are checked while plain inline form labels still pass.
|
||||
const f = await detectHtml(path.join(FIXTURES, 'modern-color-borders.html'));
|
||||
const sideTabs = f.filter(r => r.antipattern === 'side-tab');
|
||||
// Eight FLAG cases: oklch x3, oklab, lch, lab — all colored border-left
|
||||
// with a non-zero border-radius — plus two card-shaped <label> cases
|
||||
// (one oklch, one rgb). Each must produce exactly one side-tab.
|
||||
assert.equal(
|
||||
sideTabs.length, 8,
|
||||
`expected 8 side-tab findings from the FLAG column, got ${sideTabs.length}: ${sideTabs.map(r => r.snippet).join('; ')}`
|
||||
);
|
||||
// Every finding must be a border-left (never right/top/bottom) since
|
||||
// that's the only side the fixture decorates.
|
||||
for (const r of sideTabs) {
|
||||
assert.match(r.snippet || '', /border-left:/, `expected border-left, got ${r.snippet}`);
|
||||
}
|
||||
// PASS column must contribute zero border findings of either flavor.
|
||||
// There are 10 pass cases: 6 structural neutrals plus 4 labels (plain
|
||||
// inline form label, label with a neutral gray border, label in a form
|
||||
// row, and a label with a thin 1px colored left border). If any leaks
|
||||
// through, the label exception is over-broad.
|
||||
const borderAccent = f.filter(r => r.antipattern === 'border-accent-on-rounded');
|
||||
assert.equal(
|
||||
borderAccent.length, 0,
|
||||
`expected 0 border-accent-on-rounded, got ${borderAccent.length}: ${borderAccent.map(r => r.snippet).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('typography-should-flag: detects all three issues', async () => {
|
||||
const f = await detectHtml(path.join(FIXTURES, 'typography-should-flag.html'));
|
||||
assert.ok(f.some(r => r.antipattern === 'overused-font'));
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Side-Tab with Modern Color Formats (oklch/lab/lch/hsl/hwb)</title>
|
||||
<style>
|
||||
/* Two-column fixture: left col = should-flag, right col = should-pass.
|
||||
The side-tab snippet doesn't embed identifying text, so the test
|
||||
asserts finding counts against the structural ids (#flag-N / #pass-N)
|
||||
by reading the element via data attributes in the check path. Since
|
||||
that isn't available to the Node fixture runner, we instead place a
|
||||
unique number of FLAG elements (each a distinct side-tab) and verify
|
||||
the side-tab count. */
|
||||
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; background: #fafafa; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1120px; margin: 0 auto; }
|
||||
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
|
||||
.case { padding: 12px 16px; margin-bottom: 16px; }
|
||||
.case h3 { font-size: 14px; margin: 0 0 4px; }
|
||||
.case p { font-size: 13px; margin: 0; color: #64748b; }
|
||||
|
||||
/* ── FLAG cases: colored side-tab borders using modern color formats ── */
|
||||
|
||||
/* 1: oklch border-left + border-radius (the canonical real-page bug) */
|
||||
#flag-oklch-1 {
|
||||
width: 400px;
|
||||
background: oklch(100% 0 0);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid oklch(65% 0.12 250);
|
||||
}
|
||||
|
||||
/* 2: oklch mixed — neutral 1px border all around + colored 4px left +
|
||||
border-radius (mirrors .medication-card from the reproducer) */
|
||||
#flag-oklch-2 {
|
||||
width: 400px;
|
||||
background: oklch(100% 0 0);
|
||||
border: 1px solid oklch(80% 0.05 250);
|
||||
border-left: 4px solid oklch(75% 0.15 45);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 3: oklch alert pattern — colored 4px border-left + border-radius */
|
||||
#flag-oklch-3 {
|
||||
width: 400px;
|
||||
background: oklch(98% 0.01 60);
|
||||
border-left: 4px solid oklch(75% 0.18 45);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 4: oklab border-left + border-radius */
|
||||
#flag-oklab-1 {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid oklab(60% 0.1 0.15);
|
||||
}
|
||||
|
||||
/* 5: lch border-left + border-radius */
|
||||
#flag-lch-1 {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid lch(55% 60 250);
|
||||
}
|
||||
|
||||
/* 6: lab border-left + border-radius */
|
||||
#flag-lab-1 {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid lab(50% 40 -30);
|
||||
}
|
||||
|
||||
/* 7: card-shaped <label> with colored oklch side border + radius
|
||||
(the checklist-item pattern from preop-portal reproducer) */
|
||||
label#flag-label-oklch {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
width: 400px;
|
||||
padding: 16px;
|
||||
background: oklch(100% 0 0);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid oklch(65% 0.12 250);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 8: card-shaped <label> with colored rgb side border + radius —
|
||||
proves the fix is not oklch-specific */
|
||||
label#flag-label-rgb {
|
||||
display: block;
|
||||
width: 400px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── PASS cases: legitimate or neutral borders, must NOT fire ── */
|
||||
|
||||
/* 1: oklch chroma ~0 — true neutral gray border should not fire */
|
||||
#pass-oklch-neutral {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid oklch(80% 0 0);
|
||||
}
|
||||
|
||||
/* 2: oklch low chroma (0.01) — still perceptually neutral */
|
||||
#pass-oklch-nearneutral {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid oklch(75% 0.01 250);
|
||||
}
|
||||
|
||||
/* 3: oklch colored border all around (not one-sided) — no side-tab */
|
||||
#pass-oklch-allsides {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border: 3px solid oklch(65% 0.12 250);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 4: oklch colored 1px border-left (too thin to qualify) */
|
||||
#pass-oklch-thin {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 1px solid oklch(65% 0.12 250);
|
||||
}
|
||||
|
||||
/* 5: lab neutral (chroma near zero) — must not fire */
|
||||
#pass-lab-neutral {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid lab(50% 0 0);
|
||||
}
|
||||
|
||||
/* 6: lch neutral (chroma 0) */
|
||||
#pass-lch-neutral {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid lch(50% 0 0);
|
||||
}
|
||||
|
||||
/* 7: plain inline form label — no border, no padding, no radius.
|
||||
Must NOT be flagged for anything. */
|
||||
label#pass-label-plain {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 8: form label with a small neutral gray left border — neutral color
|
||||
should not fire the colored side-tab rule. */
|
||||
label#pass-label-neutral-border {
|
||||
display: block;
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 9: inline label inside a form row — default inline layout,
|
||||
no borders, must not fire. */
|
||||
label#pass-label-inline {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 10: label with 1px colored left border — too thin to qualify
|
||||
(matches the existing 1px thin rule for divs). */
|
||||
label#pass-label-thin {
|
||||
display: block;
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 1px solid #3b82f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="grid">
|
||||
<div class="col" data-col="flag">
|
||||
<h2>Should flag</h2>
|
||||
<div class="case" id="flag-oklch-1"><h3>oklch checklist</h3><p>oklch border-left 3px + radius</p></div>
|
||||
<div class="case" id="flag-oklch-2"><h3>oklch medication card</h3><p>neutral all + colored left 4px</p></div>
|
||||
<div class="case" id="flag-oklch-3"><h3>oklch alert</h3><p>warm border-left 4px + radius</p></div>
|
||||
<div class="case" id="flag-oklab-1"><h3>oklab card</h3><p>oklab border-left 4px + radius</p></div>
|
||||
<div class="case" id="flag-lch-1"><h3>lch card</h3><p>lch border-left 4px + radius</p></div>
|
||||
<div class="case" id="flag-lab-1"><h3>lab card</h3><p>lab border-left 4px + radius</p></div>
|
||||
<label class="case" id="flag-label-oklch">
|
||||
<input type="checkbox">
|
||||
<span>Card-shaped label with oklch side border</span>
|
||||
</label>
|
||||
<label class="case" id="flag-label-rgb">
|
||||
<input type="checkbox">
|
||||
<span>Card-shaped label with rgb side border</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col" data-col="pass">
|
||||
<h2>Should pass</h2>
|
||||
<div class="case" id="pass-oklch-neutral"><h3>oklch neutral</h3><p>chroma 0 gray</p></div>
|
||||
<div class="case" id="pass-oklch-nearneutral"><h3>oklch near-neutral</h3><p>chroma 0.01</p></div>
|
||||
<div class="case" id="pass-oklch-allsides"><h3>oklch all sides</h3><p>uniform border</p></div>
|
||||
<div class="case" id="pass-oklch-thin"><h3>oklch thin</h3><p>1px too thin</p></div>
|
||||
<div class="case" id="pass-lab-neutral"><h3>lab neutral</h3><p>chroma 0</p></div>
|
||||
<div class="case" id="pass-lch-neutral"><h3>lch neutral</h3><p>chroma 0</p></div>
|
||||
<div>
|
||||
<label for="pass-email" id="pass-label-plain">Email address</label>
|
||||
<input type="email" id="pass-email">
|
||||
</div>
|
||||
<label id="pass-label-neutral-border">
|
||||
<input type="checkbox">
|
||||
<span>Label with neutral gray left border</span>
|
||||
</label>
|
||||
<form>
|
||||
<label for="pass-name" id="pass-label-inline">Name:</label>
|
||||
<input type="text" id="pass-name">
|
||||
</form>
|
||||
<label id="pass-label-thin">
|
||||
<input type="checkbox">
|
||||
<span>Label with 1px colored left border (too thin)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user