mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* Fix: detect placeholder contrast (#790) `detect` never read `::placeholder` color, so pale placeholders passed. Score them with the same WCAG math as body text, without host class/clip heuristics. Prepared with AI assistance. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: match descendant ::placeholder hosts (#790) `.form ::placeholder` kept the ancestor as the host. Reuse the hover combinator star-fill so the color lands on the inputs inside. Prepared with AI assistance. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: placeholder-shown and gradient alpha (#790) Browser scans skip when :placeholder-shown is false, so a live filled field does not keep the HTML value attribute's empty state. Translucent placeholders flatten over each gradient stop before scoring. Prepared with AI assistance. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: trailing combinator only for ::placeholder hosts (#790) `star_empty_compounds` turned `.label + ::placeholder` into `.label *+*`. Fill only a trailing empty compound so adjacent-sibling hosts still match. Prepared with AI assistance. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.9 KiB
Rust
123 lines
3.9 KiB
Rust
//! Integration tests for `::placeholder` contrast detection (#790).
|
|
|
|
use impeccable_html::{detect_html_source, DetectHtmlOptions};
|
|
use std::path::Path;
|
|
|
|
const ISSUER_REPRO: &str = r#"<!DOCTYPE html>
|
|
<html><head><style>
|
|
input::placeholder { color: #bbbbbb; }
|
|
input { background: white; font-size: 16px; width: 200px; height: 40px; border: 1px solid #ccc; padding: 8px; box-sizing: border-box; }
|
|
</style></head>
|
|
<body><input placeholder="Search"></body></html>
|
|
"#;
|
|
|
|
fn scan(html: &str) -> Vec<impeccable_core::findings::Finding> {
|
|
detect_html_source(html, Path::new("/tmp/placeholder.html"), &DetectHtmlOptions::default())
|
|
}
|
|
|
|
fn repo_root() -> std::path::PathBuf {
|
|
std::env::var("IMPECCABLE_PUBLIC_REPO")
|
|
.map(std::path::PathBuf::from)
|
|
.unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
|
|
}
|
|
|
|
#[test]
|
|
fn issuer_repro_flags_pale_placeholder() {
|
|
let findings = scan(ISSUER_REPRO);
|
|
assert!(
|
|
findings.iter().any(|f| f.antipattern == "low-contrast"),
|
|
"expected low-contrast finding, got {findings:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bare_placeholder_selector_flags() {
|
|
let html = r#"<!DOCTYPE html>
|
|
<html><head><style>
|
|
::placeholder { color: #bbbbbb; }
|
|
input { background: white; font-size: 16px; width: 200px; height: 40px; }
|
|
</style></head>
|
|
<body><input placeholder="Search"></body></html>
|
|
"#;
|
|
let findings = scan(html);
|
|
assert!(
|
|
findings.iter().any(|f| f.antipattern == "low-contrast"),
|
|
"expected low-contrast for bare ::placeholder, got {findings:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn descendant_placeholder_selector_flags() {
|
|
let html = r#"<!DOCTYPE html>
|
|
<html><head><style>
|
|
.form ::placeholder { color: #bbbbbb; }
|
|
input { background: white; font-size: 16px; width: 200px; height: 40px; }
|
|
</style></head>
|
|
<body><div class="form"><input placeholder="Search"></div></body></html>
|
|
"#;
|
|
let findings = scan(html);
|
|
assert!(
|
|
findings.iter().any(|f| f.antipattern == "low-contrast"),
|
|
"expected low-contrast for descendant ::placeholder, got {findings:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sibling_placeholder_selector_flags() {
|
|
let html = r#"<!DOCTYPE html>
|
|
<html><head><style>
|
|
.label + ::placeholder { color: #bbbbbb; }
|
|
input { background: white; font-size: 16px; width: 200px; height: 40px; }
|
|
</style></head>
|
|
<body><label class="label">Name</label><input placeholder="Search"></body></html>
|
|
"#;
|
|
let findings = scan(html);
|
|
assert!(
|
|
findings.iter().any(|f| f.antipattern == "low-contrast"),
|
|
"expected low-contrast for sibling ::placeholder, got {findings:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fixture_flag_and_pass_cases() {
|
|
let fixture = repo_root().join("tests/fixtures/antipatterns/placeholder-contrast.html");
|
|
assert!(
|
|
fixture.is_file(),
|
|
"missing fixture at {}",
|
|
fixture.display()
|
|
);
|
|
let html = std::fs::read_to_string(&fixture).unwrap();
|
|
let findings = detect_html_source(&html, &fixture, &DetectHtmlOptions::default());
|
|
let ids: Vec<&str> = findings.iter().map(|f| f.antipattern.as_str()).collect();
|
|
let snippets: Vec<&str> = findings.iter().map(|f| f.snippet.as_str()).collect();
|
|
|
|
for needle in [
|
|
"Pale Placeholder On White Field",
|
|
"Pale Placeholder On White Textarea",
|
|
"Translucent Placeholder On Light Field",
|
|
"Pale Placeholder On Frosted Panel",
|
|
] {
|
|
assert!(
|
|
snippets.iter().any(|s| s.contains(needle)),
|
|
"expected flag for placeholder {needle:?}, findings={findings:?}"
|
|
);
|
|
}
|
|
|
|
for needle in [
|
|
"Ink Placeholder On White Field",
|
|
"Light Placeholder On Dark Field",
|
|
"Filled Field Hides Placeholder",
|
|
"Unstyled Placeholder Uses UA Color",
|
|
] {
|
|
assert!(
|
|
!snippets.iter().any(|s| s.contains(needle)),
|
|
"pass case {needle:?} should not flag, findings={findings:?}"
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
ids.iter().filter(|id| **id == "low-contrast").count() >= 4,
|
|
"expected at least four low-contrast hits, got {findings:?}"
|
|
);
|
|
}
|