Files
pbakaus_impeccable/tests/fixtures/antipatterns/quality.html
T
aee5ddd10c data-impeccable-ignore scoped waivers + occlusion and image-backed contrast FP fixes (#559)
* Add data-impeccable-ignore scoped waivers; fix occlusion and image-backed contrast FPs

Three changes that let a page hosting deliberate anti-pattern exhibits
scan clean without losing coverage, prepared with AI assistance (Claude
Code) on maintainer instruction:

- data-impeccable-ignore="rule-a rule-b" (or "*" / bare) on any element
  suppresses matching findings for its whole subtree, in the browser
  overlay, the extension, and the static engine. The DOM twin of the
  line-based impeccable-disable comments (which a live DOM cannot
  apply) and the generalization of data-impeccable-allow-kickers.
  Applied at the addBrowserFindings choke point, at the static element
  walk, and for regex findings that carry a live selector.

- text-occlusion: an occluder whose effective opacity multiplies out to
  ~0 paints nothing. An opacity-0 range scrubber stretched over a
  before/after comparison produced 16 "100% covered by an opaque
  element" findings on one page because elementFromPoint returns it and
  its UA background-color read as opaque paint. Invisible-at-rest
  elements are also no longer probed as victims.

- Analytic contrast now skips what it cannot measure: a url() image
  layer anywhere in the background stack ends the gradient-stops walk
  (dark ink on a bright gold-leaf image measured 2.6:1 against the wash
  composited over the wrong base), and elements that are invisible at
  rest (visibility hidden, effective opacity ~0 — hidden scene decks)
  are skipped by the color checks in both engines. The static cascade
  now tracks opacity to support this.

Covered by a new scoped-ignore fixture (exact rule, star, comma list,
nested depth, wrong-rule control) tested in both engines, a scrubber
pass case in the occlusion fixture, and image-backed / photo-panel /
hidden-scene pass cases in the gradient-ground fixture. Full suite
passes; browser and extension bundles regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CSS-scan findings carry their enclosing selector; browser pass resolves them

Page-level CSS-text findings (marquee, dark-glow, radial-halo,
repeating-stripes, codex-grid, ai-color-palette, image-hover-transform,
pseudo/inset side-tab stripes) now attach the selector of the rule that
matched, via a best-effort enclosingCssSelector() helper or the
selector already in scope. The browser pass resolves that selector
against the live DOM: pseudo segments are stripped, a selector that
renders nowhere on the page drops the finding (the CSS ships there but
the pattern never paints — the live DOM is ground truth in a browser
scan), and matches under a data-impeccable-ignore ancestor are waived.
Static scans are unchanged: partial documents keep the text-level
findings. Applied with AI assistance (Claude Code).

Covered in the scoped-ignore fixture: a live marquee under a marquee
waiver is suppressed, and dead two-axis grid CSS matching no element is
dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Attribute selectors on gradient-text and bounce-easing page emitters too

Same mechanism as the previous commit, extended to the three page-level
motion/text emitters that were still selector-less. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* text-overflow: skip SVG content; scrollWidth lies there

Chrome reports arbitrary non-zero scrollWidth/clientWidth on SVG
elements (a <text> gave 78/48 while its rendered length sat inside its
box), so the box-metric delta is noise. SVG clips to its own viewport
anyway. Pass case added to the quality fixture. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Overlay samples image-backed text at the pixel level by default

Visual contrast gains a third mode. Explicit true still runs the full
sampled pass, explicit false still disables everything (the mode the
test suites use), and unset — the default overlay run — now samples
ONLY image-backed text: the one class the analytic walk deliberately
skips, because a url() layer's pixels are unknowable without looking.

The cost is bounded and the method is precise: at most a 3x3 grid of
sample points per candidate (degrading to 3 or 1 for small rects), the
source image drawn once to a canvas with only those pixels read, and
glyph ink never pollutes the samples because the image is drawn alone.
A cross-origin image without CORS headers reports unresolved rather
than guessing. Applied with AI assistance (Claude Code).

Covered by a new fixture: white text on a near-white same-origin
data-URI image background flags via sampled pixels under default
options; dark ink on the same image passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: root opacity, keyframe steps, static parity, attributed fixtures

Applied with AI assistance (Claude Code), addressing all seven findings
from the automated reviews:

- effectiveOpacityDOM walks through body and html: a page-fade wrapper
  with body/html opacity 0 hides every descendant (Greptile executed a
  Chromium repro of the false positive).
- enclosingCssSelector refuses `from`/`to` keyframe steps, which read
  as never-matching type selectors and got valid findings wrongly
  dropped by the zero-match rule (Bugbot, high). Regression case: an
  overshoot bezier inside a `to` step must survive as page-level.
- The static cascade now inherits visibility, so descendants of a
  hidden container compute as hidden like the browser path; a declared
  visibility:visible still overrides.
- The static engine applies scoped waivers to selector-backed
  html-pattern findings, mirroring the browser — but keeps findings
  whose selector matches nothing, since static scans see partial
  documents.
- The scoped-ignore fixture grows to the mandated matrix: 4 flag cases
  (control, other-rule waiver, sibling waiver, misspelled rule id) and
  5 waived shapes (exact rule, nested depth, star, comma list, self),
  each with a unique border width so every finding attributes to
  exactly one case in both engines' tests.
- The image-backed contrast test pins its cases via the sampled
  finding's candidate text: the white-on-light specimen must flag and
  the dark-ink control must stay clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: image-only starvation, selector rejection class, inset stripes

Second review round, applied with AI assistance (Claude Code):

- The image-only filter moves inside the candidate collector, before the
  cap: gradient/opacity/filter candidates earlier in DOM order no longer
  consume the 12-candidate budget and starve the url()-backed texts the
  mode exists to sample (Bugbot, high). The regression fixture packs 14
  gradient decoys ahead of the photo panels, and the test now drives the
  overlay entry (impeccableDetectAsync, default options) rather than
  detectUrl's Node-side full fallback, which is where the image-only
  mode actually lives.
- enclosingCssSelector no longer rejects the child combinator or quoted
  attribute selectors; only braces and angle brackets disqualify.
- The inset box-shadow side-tab scanner attaches its selector like the
  pseudo-element scanner does, so those findings waive and dead-drop
  the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:31:46 -07:00

253 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quality (Typography & Readability) — Should Flag vs Should Pass</title>
<style>
/* Two-column fixture: left = should flag, right = should pass.
Covers the typography-quality rules that need real browser layout
(line-length, tight-leading, tiny-text, justified-text, all-caps-body,
wide-tracking, skipped-heading). All are browser-only — see
tests/detect-antipatterns-browser.test.mjs for assertions. */
body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #0f172a; line-height: 1.6; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1280px; margin: 0 auto; }
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
.col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }
.case { margin-bottom: 16px; padding: 16px; background: white; border: 1px solid #e2e8f0; border-radius: 8px; }
.case-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 6px; font-style: italic; }
/* ── FLAG: typography quality issues ── */
/* Line length: 12px body text fits >85 chars/line in a ~528px column.
12px is not below the tiny-text 12px floor (rule fires at <12px). */
.long-lines {
font-size: 12px;
line-height: 1.6;
}
/* Tight leading */
.tight-leading {
font-size: 16px;
line-height: 1.0;
max-width: 40em;
}
/* Tiny body text */
.tiny-text {
font-size: 10px;
line-height: 1.6;
max-width: 40em;
}
.tiny-text-11 {
font-size: 11px;
line-height: 1.6;
max-width: 40em;
}
/* Justified text without hyphens */
.justified-no-hyphens {
text-align: justify;
max-width: 30em;
font-size: 16px;
line-height: 1.6;
}
/* All-caps long body */
.all-caps-body {
text-transform: uppercase;
font-size: 16px;
max-width: 40em;
line-height: 1.6;
}
/* Wide letter spacing on body */
.wide-tracking-body {
letter-spacing: 0.15em;
font-size: 16px;
max-width: 40em;
line-height: 1.6;
}
/* ── PASS: comfortable typography ── */
.good-measure {
max-width: 65ch;
font-size: 16px;
line-height: 1.6;
}
.good-leading {
font-size: 16px;
line-height: 1.6;
max-width: 40em;
}
.good-text-size {
font-size: 16px;
line-height: 1.6;
max-width: 40em;
}
/* Justified text WITH hyphens — fine */
.justified-with-hyphens {
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
max-width: 30em;
font-size: 16px;
line-height: 1.6;
}
/* Short label in all-caps (passes — under 30 chars) */
.label-caps {
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.1em;
color: #475569;
}
/* Wide tracking on a short uppercase label (passes — both conditions excluded) */
.label-tracking {
letter-spacing: 0.15em;
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
color: #475569;
}
.mock-terminal-title,
.mock-terminal-meta,
.mock-diff-line {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: #475569;
line-height: 1.5;
}
.mock-terminal-title { font-size: 11px; }
.mock-terminal-meta { font-size: 10.8px; }
.mock-diff-line { font-size: 11.84px; }
</style>
</head>
<body>
<div class="grid">
<!-- ════════════════════════════════════════════════════════════
LEFT COLUMN: should flag
═══════════════════════════════════════════════════════════ -->
<div class="col" data-col="flag">
<h2>Should flag</h2>
<h3>Line length too long</h3>
<div class="case">
<span class="case-label">no max-width on a paragraph</span>
<p class="long-lines">This paragraph has no max-width constraint at all, which means on a wide monitor or ultrawide display, each line of text can stretch to 150 or even 200 characters wide. Research consistently shows that line lengths beyond 75 characters significantly reduce reading speed and comprehension. The eye has to travel too far to find the beginning of the next line, causing readers to lose their place. A simple max-width of 65ch to 75ch on the paragraph or its container would fix this entirely.</p>
</div>
<h3>Tight line height</h3>
<div class="case">
<span class="case-label">line-height: 1.0</span>
<p class="tight-leading">This paragraph has a line-height of 1.0, which means the lines are touching. Multi-line body text needs breathing room between lines for readability. A line-height of 1.5 to 1.7 is generally recommended for body text.</p>
</div>
<h3>Tiny body text</h3>
<div class="case">
<span class="case-label">10px body text</span>
<p class="tiny-text">This body text is only 10px. While this might be fine for a disclaimer or legal footnote, it's too small for primary content that users need to actually read. Aim for at least 14px for body content, 16px is ideal.</p>
</div>
<h3>Justified text without hyphens</h3>
<div class="case">
<span class="case-label">text-align: justify, no hyphens: auto</span>
<p class="justified-no-hyphens">This paragraph uses text-align: justify, which forces each line to stretch to fill the full width. Without hyphenation support, this creates uneven gaps between words known as "rivers of white space" that flow vertically through the text. Left-aligned text is almost always more readable on the web.</p>
</div>
<h3>All-caps body text</h3>
<div class="case">
<span class="case-label">text-transform: uppercase on a long passage</span>
<p class="all-caps-body">This entire paragraph is in uppercase via text-transform. While all-caps works for short labels, headings, or navigation items, longer body text in uppercase is significantly harder to read because we lose the word shape cues that come from ascenders and descenders in mixed-case text.</p>
</div>
<h3>Wide letter spacing on body text</h3>
<div class="case">
<span class="case-label">letter-spacing: 0.15em on body text</span>
<p class="wide-tracking-body">This body text has letter-spacing: 0.15em applied to it. While subtle tracking adjustments can improve readability for headings or all-caps text, adding significant letter spacing to body text actually makes it harder to read by disrupting natural character groupings.</p>
</div>
<h3>Skipped heading levels</h3>
<div class="case">
<span class="case-label">h1 → h3 (missing h2)</span>
<h1 style="font-size: 20px; margin: 0 0 4px;">Top heading</h1>
<h3 style="font-size: 16px; margin: 0;">Skips straight to h3</h3>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════
RIGHT COLUMN: should pass
═══════════════════════════════════════════════════════════ -->
<div class="col" data-col="pass">
<h2>Should pass</h2>
<h3>Comfortable line length</h3>
<div class="case">
<span class="case-label">max-width: 65ch on the paragraph</span>
<p class="good-measure">This paragraph has a max-width of 65ch, keeping the line length comfortable for reading. The eye can easily track from the end of one line to the beginning of the next without losing its place.</p>
</div>
<h3>Comfortable line height</h3>
<div class="case">
<span class="case-label">line-height: 1.6</span>
<p class="good-leading">This paragraph has a line-height of 1.6, which gives multi-line text plenty of room to breathe and improves the rhythm of the page.</p>
</div>
<h3>Comfortable body text size</h3>
<div class="case">
<span class="case-label">16px body text</span>
<p class="good-text-size">This is 16px body text — the recommended baseline for comfortable reading on modern displays.</p>
</div>
<h3>Justified text with hyphens</h3>
<div class="case">
<span class="case-label">text-align: justify + hyphens: auto</span>
<p class="justified-with-hyphens">When justified text is paired with hyphens: auto, the browser can break long words across lines, eliminating the rivers of white space and making the justification look intentional rather than awkward.</p>
</div>
<h3>Short label in all-caps</h3>
<div class="case">
<span class="case-label">short label, uppercase</span>
<span class="label-caps">Featured</span>
</div>
<h3>Wide tracking on a short uppercase label</h3>
<div class="case">
<span class="case-label">letter-spacing on a short uppercase label</span>
<span class="label-tracking">Beta</span>
</div>
<h3>Proper heading hierarchy</h3>
<div class="case">
<span class="case-label">h1 → h2 → h3</span>
<h1 style="font-size: 20px; margin: 0 0 4px;">Top heading</h1>
<h2 style="font-size: 18px; margin: 0 0 4px;">Second level</h2>
<h3 style="font-size: 16px; margin: 0;">Third level</h3>
</div>
<h3>Small mockup metadata</h3>
<div class="case">
<span class="case-label">terminal/diff metadata at tiny sizes</span>
<span class="mock-terminal-title">/impeccable polish : scanning codebase</span>
<div class="mock-terminal-meta">12 files inspected · 4 visual issues queued</div>
<div class="mock-diff-line">+ Replace generic button label with action-specific copy</div>
</div>
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
<!-- SVG text: Chrome reports fake scrollWidth/clientWidth on SVG content;
the text-overflow rule must skip it (pass). -->
<svg class="svg-metrics-pass" viewBox="0 0 430 60" width="430" height="60" role="img" aria-label="diagram">
<rect x="6" y="10" width="96" height="40" rx="3" fill="none" stroke="#888"/>
<text x="54" y="35" text-anchor="middle" font-size="13">impeccable</text>
</svg>
</body>
</html>