Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/preview/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,24 @@ const SLOP_AUDIT = `(() => {
const cardish = kids.every((k) => k.querySelector('svg, img') && k.querySelector('h2,h3,h4') && k.querySelector('p'));
if (sameSize && cardish) { out.push('identical icon-card grid (' + kids.length + ' cards)'); break; }
}
// Reduced-motion respect: infinite CSS animations with no
// prefers-reduced-motion guard anywhere spin forever for users who
// asked motion to stop (vestibular triggers).
let infiniteAnims = 0;
for (const el of document.querySelectorAll('*')) {
if (infiniteAnims > 0) break;
const cs = getComputedStyle(el);
if (cs.animationIterationCount === 'infinite' && cs.animationName !== 'none' && parseFloat(cs.animationDuration) > 0) infiniteAnims++;
}
if (infiniteAnims > 0) {
let guarded = false;
for (const sheet of document.styleSheets) {
let rules; try { rules = sheet.cssRules; } catch { continue; }
for (const r of rules) { if (r.media && /prefers-reduced-motion/.test(r.media.mediaText || '')) { guarded = true; break; } }
if (guarded) break;
}
if (!guarded) out.push('reduced motion: infinite animation(s) run with no prefers-reduced-motion guard anywhere — they never stop for users who disabled motion');
}
let emojiBullets = 0;
for (const li of document.querySelectorAll('li')) {
if (/^[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/u.test((li.textContent || '').trim())) emojiBullets++;
Expand Down
4 changes: 4 additions & 0 deletions test/cdp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ describe.skipIf(!chrome || !hasWebSocket())('cdpCapture (requires Chrome + WebSo
.c1{color:#111}.c2{color:#222}.c3{color:#333}.c4{color:#444}.c5{color:#555}
.c6{color:#666}.c7{color:#777}.c8{color:#888}.c9{color:#999}.c10{color:#aaa}
.real-thing{font-weight:bold}
@keyframes spin { to { transform: rotate(360deg) } }
.spinner { animation: spin 1s linear infinite }
@font-face { font-family: 'Slopsans'; src: url('slopsans.woff2'); }
.card-a{view-transition-name:hero-card}
.card-b{view-transition-name:hero-card}
Expand All @@ -93,6 +95,7 @@ describe.skipIf(!chrome || !hasWebSocket())('cdpCapture (requires Chrome + WebSo
<input type="text" />
<button class="bad-focus" style="outline:none">go</button>
<ul><li>🚀 fast</li><li>✨ shiny</li><li>🔥 hot</li></ul>
<div class="spinner">loading</div>
<a class="cta" style="background-color:#6d4aff">Get started</a>
<button style="background-color:#7c3aed">Sign up now</button>
<div class="card-a">a</div><div class="card-b">b</div><div class="tooltip">tip</div>
Expand Down Expand Up @@ -189,6 +192,7 @@ describe.skipIf(!chrome || !hasWebSocket())('cdpCapture (requires Chrome + WebSo
expect(slop).toContain('generic font stack: arial')
expect(slop).toContain('emoji-bulleted')
expect(slop).toContain('the Purple Problem')
expect(slop).toContain('reduced motion: infinite animation')
expect(slop).toContain('font loading: @font-face "Slopsans" has no font-display')
expect(slop).toContain('template CTA copy: "Get started"')

Expand Down