Merge pull request #415 from pbakaus/css-pseudo-stripe-coverage

Detect pseudo-element stripes in standalone stylesheets and style blocks
This commit is contained in:
Paul Bakaus
2026-07-25 18:39:24 -07:00
committed by GitHub
6 changed files with 257 additions and 4 deletions
@@ -86,6 +86,74 @@ describe('detectText - Astro structural CSS fixtures', () => {
});
});
describe('detectText — pseudo-element stripe fixtures (issue #394)', () => {
// The side-tab silhouette drawn as an absolutely-positioned ::before/::after
// bar instead of a border. The scanner already ran on full HTML pages via
// checkHtmlPatterns; these pin the standalone-stylesheet and component
// style-block paths, which used to pass this construction clean.
const SHOULD_FLAG = [
'Inset Shorthand Left Edge',
'Longhand Left Edge',
'Bottom Edge',
'Full Height Right Edge',
];
const SHOULD_PASS = [
'Neutral Divider',
'Wide Panel',
'Static Underline',
'Hairline Divider',
'Hover Underline',
'Floating Badge',
// Commented-out CSS is not a live rule.
'Commented Out Stripe',
];
// The 1-based line a case's selector sits on in a fixture file, so the
// reported finding line can be checked against the actual source.
const selectorLine = (source, caseName) => {
const idx = source.split('\n').findIndex(l => l.includes(`data-case="${caseName}"`));
assert.notEqual(idx, -1, `fixture is missing case "${caseName}"`);
return idx + 1;
};
it('standalone .css files flag chromatic pseudo-element stripes only', () => {
const filePath = path.join(FIXTURES, 'pseudo-stripe.css');
const source = fs.readFileSync(filePath, 'utf8');
const findings = detectText(source, filePath).filter(r => r.antipattern === 'side-tab');
const snippets = findings.map(r => r.snippet || '').join(' | ');
for (const heading of SHOULD_FLAG) {
assert.match(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `expected "${heading}" to flag`);
}
for (const heading of SHOULD_PASS) {
assert.doesNotMatch(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `"${heading}" should pass`);
}
// Every finding must carry the selector's real source line, so
// line-scoped inline ignores (impeccable-disable-line and
// impeccable-disable-next-line) can match it.
for (const f of findings) {
const caseName = (f.snippet.match(/data-case="([^"]+)"/) || [])[1];
assert.equal(
f.line, selectorLine(source, caseName),
`finding for "${caseName}" reports line ${f.line}, selector sits on line ${selectorLine(source, caseName)}`,
);
}
});
it('component style blocks flag pseudo-element stripes at their source line', () => {
const filePath = path.join(FIXTURES, 'pseudo-stripe.vue');
const source = fs.readFileSync(filePath, 'utf8');
const findings = detectText(source, filePath).filter(r => r.antipattern === 'side-tab');
const snippets = findings.map(r => r.snippet || '').join(' | ');
assert.match(snippets, /data-case="Component Left Edge"/, 'expected the component stripe to flag');
assert.doesNotMatch(snippets, /data-case="Component Neutral Divider"/, 'neutral divider should pass');
const stripe = findings.find(r => /data-case="Component Left Edge"/.test(r.snippet || ''));
assert.equal(
stripe.line, selectorLine(source, 'Component Left Edge'),
'style-block finding must map back to the whole-file line, not the block-local one',
);
});
});
describe('detectHtml — static HTML/CSS fixtures', () => {
it('should-flag: catches border anti-patterns', async () => {
const f = await detectHtml(path.join(FIXTURES, 'should-flag.html'));
+120
View File
@@ -0,0 +1,120 @@
/*
* Pseudo-element stripe fixture for the regex engine (issue #394).
*
* The side-tab silhouette built as an absolutely-positioned ::before/::after
* bar instead of a border. scanCssTextForPseudoStripe already caught these on
* full HTML pages; this fixture pins the standalone-stylesheet path. The
* data-case attribute in each selector lands in the finding snippet, so the
* test can attribute every flag/pass case individually.
*/
:root {
--stripe-fixture-neutral: #e5e7eb;
}
/* ── FLAG: the issue reproducer — inset shorthand pin + 4px chromatic bar ── */
.card[data-case="Inset Shorthand Left Edge"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: #7c3aed;
}
/* ── FLAG: longhand edge pins; unresolvable var() errs toward detection ── */
.card[data-case="Longhand Left Edge"]::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 4px;
background: var(--accent);
}
/* ── FLAG: horizontal variant riding the bottom edge ── */
.card[data-case="Bottom Edge"]::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 4px;
background: #f43f5e;
}
/* ── FLAG: height:100% full-height stripe on the right edge ── */
.card[data-case="Full Height Right Edge"]::before {
content: "";
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 3px;
background: oklch(60% 0.2 300);
}
/* ── PASS: neutral gray divider is not an accent stripe ── */
.card[data-case="Neutral Divider"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--stripe-fixture-neutral);
}
/* ── PASS: 24px is a panel, not a stripe ── */
.card[data-case="Wide Panel"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 24px;
background: #7c3aed;
}
/* ── PASS: no position: absolute — not an overlay stripe ── */
.card[data-case="Static Underline"]::before {
content: "";
width: 4px;
background: #7c3aed;
}
/* ── PASS: 1px hairline is a divider ── */
.card[data-case="Hairline Divider"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 1px;
background: #7c3aed;
}
/* ── PASS: hover-conditional underline is an affordance, not decoration ── */
.link-row[data-case="Hover Underline"]:hover::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 4px;
background: #7c3aed;
}
/* ── PASS: pinned to one edge but not full-height — a badge, not a stripe ── */
.card[data-case="Floating Badge"]::before {
content: "";
position: absolute;
left: 0;
width: 4px;
height: 12px;
background: #7c3aed;
}
/* ── PASS: commented-out CSS is not a live rule ──
.card[data-case="Commented Out Stripe"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: #7c3aed;
}
*/
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="card">
<div class="body">Component with a pseudo-element stripe in its style block</div>
</div>
</template>
<script setup>
const label = 'pseudo-stripe fixture';
</script>
<style scoped>
/* FLAG: same silhouette as a border side-tab, drawn as a ::before bar */
.card[data-case="Component Left Edge"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: #7c3aed;
}
/* PASS: neutral hairline divider */
.card[data-case="Component Neutral Divider"]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 1px;
background: #e5e7eb;
}
</style>