Skip to content

Commit 4e0cea6

Browse files
committed
Add a test for contrast checking and fix alignment issues in mobile mode
1 parent 307019c commit 4e0cea6

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

src/curl_fuzzer_tools/templates/corpus_decoder.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@
198198
color: rgba(255, 255, 255, 0.7);
199199
}
200200

201+
.summary-item dt {
202+
color: rgba(255, 255, 255, 0.7);
203+
}
204+
201205
.card {
202206
background: rgba(25, 25, 25, 0.75);
203207
border-color: rgba(255, 255, 255, 0.1);
@@ -351,6 +355,12 @@
351355
align-items: flex-start;
352356
}
353357

358+
/* In preview/hex cells, render inline content normally to avoid one-span-per-line */
359+
tbody tr td.mono {
360+
display: block;
361+
align-items: initial;
362+
}
363+
354364
tbody tr[data-entry] td:first-child {
355365
display: none;
356366
}
@@ -379,6 +389,7 @@
379389
.mono {
380390
white-space: pre-wrap;
381391
word-break: break-word;
392+
overflow-wrap: anywhere;
382393
font-size: 0.9rem;
383394
}
384395
}

tests/browser/test_corpus_decoder.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Playwright integration test for the generated corpus decoder HTML."""
22

33
from pathlib import Path
4+
from typing import Literal
45

56
import pytest
67

@@ -57,3 +58,115 @@ def test_upload_repository_corpus(tmp_path: Path) -> None:
5758
assert rows.count() == expected_tlvs
5859

5960
browser.close()
61+
62+
63+
@pytest.mark.skipif(sync_playwright is None, reason="Playwright not installed")
64+
@pytest.mark.parametrize("scheme", ["light", "dark"])
65+
def test_accessibility_after_upload_in_light_and_dark(tmp_path: Path, scheme: Literal["light", "dark"]) -> None:
66+
"""Basic accessibility smoke: after upload, key elements are visible in both schemes.
67+
68+
This test toggles prefers-color-scheme and checks that:
69+
- The dark/light CSS actually applies (by inspecting body background color in dark)
70+
- Headings and summary items remain present
71+
- A coarse contrast check (>= 3.0) passes between body background and heading text
72+
to catch regressions where text becomes unreadable.
73+
"""
74+
html_path = tmp_path / "index.html"
75+
generate_html(html_path)
76+
77+
corpus_path = _example_corpus()
78+
expected_tlvs = _expected_tlvs(corpus_path)
79+
80+
file_url = html_path.resolve().as_uri()
81+
82+
if sync_playwright is None:
83+
pytest.skip("Playwright not installed")
84+
85+
with sync_playwright() as playwright:
86+
browser = playwright.chromium.launch()
87+
page = browser.new_page()
88+
page.emulate_media(color_scheme=scheme) # Apply requested color scheme
89+
page.goto(file_url)
90+
91+
# Upload corpus and wait for summary
92+
page.set_input_files("#corpus-input", str(corpus_path))
93+
page.wait_for_selector(f"text=Decoded {expected_tlvs} TLVs successfully.")
94+
95+
# Verify headings and summary exist
96+
assert page.locator("header h1").count() == 1
97+
assert page.locator("#summary-count").inner_text().strip() == str(expected_tlvs)
98+
99+
# Page-wide contrast sweep over visible text nodes; collect failures (< 3.0)
100+
results = page.evaluate(
101+
r"""
102+
() => {
103+
function parseColor(c) {
104+
const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/);
105+
if (!m) return {r:0,g:0,b:0,a:1};
106+
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] };
107+
}
108+
function blend(top, bottom) {
109+
// Alpha composite 'top' over 'bottom'; both are {r,g,b,a} with a in [0,1]
110+
const a = top.a + bottom.a * (1 - top.a);
111+
const r = Math.round((top.r * top.a + bottom.r * bottom.a * (1 - top.a)) / (a || 1));
112+
const g = Math.round((top.g * top.a + bottom.g * bottom.a * (1 - top.a)) / (a || 1));
113+
const b = Math.round((top.b * top.a + bottom.b * bottom.a * (1 - top.a)) / (a || 1));
114+
return { r, g, b, a: 1 };
115+
}
116+
function srgbToLin(v) {
117+
v /= 255;
118+
return v <= 0.04045 ? v/12.92 : Math.pow((v + 0.055)/1.055, 2.4);
119+
}
120+
function relLuma({r,g,b}) {
121+
const R = srgbToLin(r), G = srgbToLin(g), B = srgbToLin(b);
122+
return 0.2126*R + 0.7152*G + 0.0722*B;
123+
}
124+
function isVisible(el) {
125+
const cs = getComputedStyle(el);
126+
const rect = el.getBoundingClientRect();
127+
return rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.display !== 'none' && parseFloat(cs.opacity) > 0.05;
128+
}
129+
function bodyBg() {
130+
let b = parseColor(getComputedStyle(document.body).backgroundColor);
131+
if (b.a === 0) b = { r: 255, g: 255, b: 255, a: 1 };
132+
return b;
133+
}
134+
function effectiveBackground(el) {
135+
if (!el) return bodyBg();
136+
const cs = getComputedStyle(el);
137+
const bg = parseColor(cs.backgroundColor);
138+
if (bg.a === 0) return effectiveBackground(el.parentElement);
139+
const parentBg = effectiveBackground(el.parentElement);
140+
if (bg.a >= 1) return bg;
141+
return blend(bg, parentBg);
142+
}
143+
const nodes = Array.from(document.querySelectorAll('*'));
144+
const failures = [];
145+
let scanned = 0;
146+
for (const el of nodes) {
147+
if (!isVisible(el)) continue;
148+
const text = (el.textContent || '').trim();
149+
if (!text) continue;
150+
const cs = getComputedStyle(el);
151+
let fg = parseColor(cs.color);
152+
const bg = effectiveBackground(el);
153+
if (fg.a === 0) continue; // fully transparent text
154+
if (fg.a < 1) fg = blend(fg, bg);
155+
const L1 = relLuma(fg);
156+
const L2 = relLuma(bg);
157+
const contrast = (Math.max(L1,L2)+0.05) / (Math.min(L1,L2)+0.05);
158+
scanned += 1;
159+
if (contrast < 3.0) {
160+
failures.push({ tag: el.tagName.toLowerCase(), text: text.slice(0, 60), contrast: Math.round(contrast*100)/100 });
161+
}
162+
}
163+
return { scanned, failures, minContrast: failures.length ? Math.min(...failures.map(f=>f.contrast)) : null };
164+
}
165+
"""
166+
)
167+
assert results and isinstance(results, dict)
168+
assert results.get("scanned", 0) > 0
169+
failed = results.get("failures", [])
170+
assert not failed, f"Low contrast elements in {scheme} mode: {failed[:3]}{(' …' if len(failed) > 3 else '')}"
171+
172+
browser.close()

0 commit comments

Comments
 (0)