Sampled contrast: no panic on multibyte urls, any layer order, auto size

Second review round for the sampled-contrast path (#560), plus the
Windows CI fix:

- `is_data_uri` byte-sliced the url, so a local image whose name opens
  with a multibyte character panicked the static scanner. It reads the
  prefix with `get` now.
- The ground walk only accepted a layer that begins with `url(`, while a
  `background` shorthand may put color, position, size, or repeat tokens
  before the image; such layers fell out of the sampled path. The call is
  found anywhere in the layer and decoded from there. A fixture case pins
  it, so the fixture and directory goldens gain one finding.
- The decoration gate scaled the missing size axis only for a single
  `40px` token; `40px auto` and `auto 40px` now follow the image's aspect
  ratio the same way.
- The linked-sheet url rewrite did its path math through the OS-flavored
  helpers, which rendered `css/light.png` as `light.png` on Windows. It
  uses the POSIX helpers on both directories, since a CSS url is POSIX on
  every OS.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-10 10:43:07 +05:00
co-authored by Claude Fable 5.1
parent 66945a79da
commit 0f115e5d37
13 changed files with 57 additions and 27 deletions
+18 -9
View File
@@ -79,15 +79,16 @@ pub fn is_decorative_layer(repeat: &str, size: &str, intrinsic_w: f64, intrinsic
}
let tokens: Vec<&str> = size.split_whitespace().collect();
let px = |t: Option<&&str>| t.filter(|t| t.ends_with("px")).map(|t| parse_float(t));
let w = px(tokens.first()).unwrap_or(intrinsic_w);
let h = px(tokens.get(1)).unwrap_or_else(|| {
// `background-size: 40px` scales the height with the width.
if tokens.len() == 1 && px(tokens.first()).is_some() && intrinsic_w > 0.0 {
intrinsic_h * (w / intrinsic_w)
} else {
intrinsic_h
}
});
// An `auto` (or absent) axis follows the other at the image's aspect
// ratio, as in CSS: `40px`, `40px auto`, and `auto 40px` all scale.
let (w, h) = match (px(tokens.first()), px(tokens.get(1))) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) if intrinsic_w > 0.0 => (w, intrinsic_h * (w / intrinsic_w)),
(None, Some(h)) if intrinsic_h > 0.0 => (intrinsic_w * (h / intrinsic_h), h),
(Some(w), None) => (w, intrinsic_h),
(None, Some(h)) => (intrinsic_w, h),
(None, None) => (intrinsic_w, intrinsic_h),
};
(!repeat_x && w < DECORATION_MAX_PX) || (!repeat_y && h < DECORATION_MAX_PX)
}
@@ -213,6 +214,14 @@ mod tests {
fn decoration_gate() {
assert!(is_decorative_layer("no-repeat", "auto", 24.0, 24.0));
assert!(is_decorative_layer("no-repeat", "40px", 1200.0, 800.0));
assert!(is_decorative_layer("no-repeat", "40px auto", 1200.0, 800.0));
assert!(is_decorative_layer("no-repeat", "auto 40px", 1200.0, 800.0));
assert!(!is_decorative_layer(
"no-repeat",
"auto 800px",
1200.0,
800.0
));
assert!(is_decorative_layer("repeat-y", "auto", 8.0, 400.0));
assert!(is_decorative_layer("repeat-x", "auto", 1200.0, 80.0));
assert!(!is_decorative_layer("no-repeat", "cover", 24.0, 24.0));
+12 -7
View File
@@ -363,12 +363,14 @@ fn layer_entry(value: &str, index: usize) -> String {
}
/// The `url()` argument of one background layer: `url(x\ y.png)` from the
/// css-tree generator, `url("x y.png")` from a style attribute. The layer
/// may carry the rest of a shorthand after the call (`url(x)center/cover`),
/// css-tree generator, `url("x y.png")` from a style attribute. A shorthand
/// layer may carry other tokens around the call (`#000 url(x)center/cover`),
/// so only the call itself is decoded.
fn layer_url(layer: &str) -> String {
let layer = js::trim(layer);
let open = layer.find('(').map(|i| i + 1).unwrap_or(layer.len());
let Some(call) = URL_CALL_RE.find(layer) else {
return String::new();
};
let open = call.end();
let mut depth = 1usize;
let mut quote: Option<char> = None;
let mut close = layer.len();
@@ -388,7 +390,7 @@ fn layer_url(layer: &str) -> String {
None => {}
}
}
let decoded = decode_url(&layer[..close]);
let decoded = decode_url(&format!("url({}", &layer[open..close]));
let trimmed = js::trim(&decoded);
let unquoted = trimmed
.strip_prefix('"')
@@ -433,7 +435,10 @@ pub fn find_image_ground(el: &StaticElement<'_>) -> Option<ImageGround> {
};
for (index, layer) in layers.iter().enumerate() {
let layer = js::trim(layer);
if URL_START_RE.is_match(layer) {
// A shorthand layer may put color, position, size, or repeat
// tokens before the image call; the call is what matters.
let is_gradient = GRADIENT_CALL_RE.is_match(layer);
if !is_gradient && URL_CALL_RE.is_match(layer) {
// A layer beneath the image is unknown paint; otherwise the
// element's own color over its parent's ground, white at the
// root like the analytic walk.
@@ -459,7 +464,7 @@ pub fn find_image_ground(el: &StaticElement<'_>) -> Option<ImageGround> {
under,
});
}
if GRADIENT_CALL_RE.is_match(layer) {
if is_gradient {
let stops = parse_gradient_colors(Some(layer));
if stops.is_empty() || stops.iter().all(|s| s.alpha_or_one() >= 0.99) {
return None;
+7 -2
View File
@@ -176,8 +176,13 @@ pub fn rewrite_sheet_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String
}
let cut = target.find(['?', '#']).unwrap_or(target.len());
let (path, suffix) = target.split_at(cut);
let absolute = jsp::resolve("/", &[sheet_dir, path]);
let relative = jsp::to_posix(&jsp::relative("/", page_dir, &absolute));
// Pure POSIX string math on both directories: a CSS url is
// POSIX on every OS, and the win32 helpers would render a drive
// path (`D:\a\...`) differently from the url beside it.
let sheet = jsp::to_posix(sheet_dir);
let page = jsp::to_posix(page_dir);
let absolute = jsp::posix::resolve("/", &[&sheet, path]);
let relative = jsp::posix::relative("/", &page, &absolute);
if relative.is_empty() {
return whole.to_string();
}
+8 -1
View File
@@ -121,7 +121,9 @@ impl ImageSampler {
}
fn is_data_uri(url: &str) -> bool {
url.len() > 5 && url[..5].eq_ignore_ascii_case("data:")
// `get`, not a byte slice: a url may open with a multibyte character.
url.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
}
/// The cache key of a data URI: its length and a hash, so the cache never
@@ -242,6 +244,11 @@ mod tests {
assert!(sampler.load("https://example.com/hero.jpg").is_none());
assert!(sampler.load("//cdn.example.com/hero.jpg").is_none());
assert_eq!(ground_label("img/hero.jpg?v=3"), "hero.jpg");
// A url that opens with a multibyte character is a local path, not
// a panic.
assert!(!is_data_uri("dat\u{20ac}"));
assert!(sampler.load("abcd\u{20ac}.png").is_none());
assert_eq!(ground_label("abcd\u{20ac}.png"), "abcd\u{20ac}.png");
// A payload past the byte budget is refused before it is decoded.
let huge = format!(
"data:image/png;base64,{}",
@@ -20,6 +20,7 @@
.panel h2 { margin: 0; font-size: 28px; line-height: 1.3; }
.light { background: url(sampled-images/light.png); }
.light-webp { background: url("sampled-images/light.webp") center / cover; }
.tokens-first { background: center / cover no-repeat url(sampled-images/light.png); }
.dark-jpg { background: url(sampled-images/dark.jpg) center / cover no-repeat; }
.split { background: url(sampled-images/split.png) repeat; }
.inline-data { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAaUlEQVR42k2PSQrAMAwD/f9ndt+TNl2OLmMQ5CCELUtR7CuLP+cUuPPo19F73tqY09q4nXsXC0SYA8T3mgNxgMiypMGZccLA5Kij6+eMDhKIZEZQsmkBA47UKxLkFKsHjCE6IOBQSv2bH8D48hUetch/AAAAAElFTkSuQmCC); }
@@ -73,6 +74,9 @@
<div class="panel light">
<h2 class="white">Large white heading on the texture</h2>
</div>
<div class="panel tokens-first">
<p class="white">Shorthand tokens before the image still name the ground.</p>
</div>
<!-- ── Should pass ──────────────────────────────────────────────── -->
<div class="panel light">
+1 -1
View File
@@ -172,7 +172,7 @@ text instead of skipping the contrast check (docs/CLI-CONTRACT.md, "Sampled
contrast"). The JS never had this path, so the new fixture
`sampled-image-contrast.html` (with `sampled-images/`) has no JS golden; its
cases were recorded from the engine, and the directory sweeps below moved
only by that fixture's eight `low-contrast` findings, verified entry by entry.
only by that fixture's nine `low-contrast` findings, verified entry by entry.
- `detect-fixture-json-sampled-image-contrast-html`, `detect-fixture-text-sampled-image-contrast-html`, `detect-fixture-json-sampled-images`, `detect-fixture-text-sampled-images`: new cases.
- `detect-dir-json-all-fixtures`, `detect-dir-text-all-fixtures`, `detect-dir-quiet-all-fixtures`, `detect-no-advisory-json`, `detect-no-advisory-text`: the directory sweep picks up the new fixture.
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
{
"stdout": "",
"stderr": "423 anti-patterns found.\n17 advisory notes (not counted).\n",
"stderr": "424 anti-patterns found.\n17 advisory notes (not counted).\n",
"exit": 2,
"signal": null,
"files": {}
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
{
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.3:1 (need 4.5:1) — text #3a3632 on dark.jpg; p90 of 36 samples, median 1.2:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.webp; p90 of 36 samples, median 1.2:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on data:image/png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 2.5:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 2.4:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.8:1 (need 4.5:1) — text #262421 on cutout.png; p90 of 36 samples, median 1.8:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 3:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n }\n]\n",
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.3:1 (need 4.5:1) — text #3a3632 on dark.jpg; p90 of 36 samples, median 1.2:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.webp; p90 of 36 samples, median 1.2:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on data:image/png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 2.5:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 2.4:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.8:1 (need 4.5:1) — text #262421 on cutout.png; p90 of 36 samples, median 1.8:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 3:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\",\n \"line\": 0,\n \"snippet\": \"sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\"\n }\n]\n",
"stderr": "",
"exit": 2,
"signal": null,
@@ -1,6 +1,6 @@
{
"stdout": "",
"stderr": "\n<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.3:1 (need 4.5:1) — text #3a3632 on dark.jpg; p90 of 36 samples, median 1.2:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.webp; p90 of 36 samples, median 1.2:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on data:image/png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 2.5:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 2.4:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.8:1 (need 4.5:1) — text #262421 on cutout.png; p90 of 36 samples, median 1.8:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 3:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n\n8 anti-patterns found.\n",
"stderr": "\n<REPO>/tests/fixtures/antipatterns/sampled-image-contrast.html\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.3:1 (need 4.5:1) — text #3a3632 on dark.jpg; p90 of 36 samples, median 1.2:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.webp; p90 of 36 samples, median 1.2:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on data:image/png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 2.5:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 2.4:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.8:1 (need 4.5:1) — text #262421 on cutout.png; p90 of 36 samples, median 1.8:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 3:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] sampled (coarse) 1.2:1 (need 4.5:1) — text #fdfdfd on light.png; p90 of 36 samples, median 1.1:1\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n\n9 anti-patterns found.\n",
"exit": 2,
"signal": null,
"files": {}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long