Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
115 changes: 115 additions & 0 deletions crates/browser/tests/round11_edge_cases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! Corpus round 11's deferred edge cases (#1076), over three fixtures in
//! `tests/fixtures/antipatterns/`, rendered by an installed browser. Skips
//! cleanly when there is none.

use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};

use impeccable_browser::BrowserEngine;
use impeccable_detect::engines::{ScanOptions, UrlEngine};

fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/antipatterns")
}

fn serve() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
std::thread::spawn(move || handle(stream));
}
});
port
}

fn handle(mut stream: TcpStream) {
let mut buf = [0u8; 8192];
let n = stream.read(&mut buf).unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);
let rel = request
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.unwrap_or("/")
.trim_start_matches('/')
.to_string();
let (status, body) = match std::fs::read(fixtures_dir().join(&rel)) {
Ok(body) if !rel.contains("..") => ("200 OK", body),
_ => ("404 Not Found", b"missing".to_vec()),
};
let head = format!(
"HTTP/1.0 {status}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = stream.write_all(head.as_bytes());
let _ = stream.write_all(&body);
let _ = stream.flush();
}

fn engine() -> Option<BrowserEngine> {
let env: HashMap<String, String> = std::env::vars().collect();
if impeccable_browser::discovery::find_browser(&env).is_err() {
eprintln!("skip: no installed browser found");
return None;
}
Some(BrowserEngine::new(env))
}

fn scan(engine: &BrowserEngine, port: u16, fixture: &str) -> Vec<impeccable_core::findings::Finding> {
let url = format!("http://127.0.0.1:{port}/{fixture}");
engine.detect_url(&url, &ScanOptions::default()).expect("scan")
}

fn selector(f: &impeccable_core::findings::Finding) -> &str {
f.extras.get("selector").and_then(|v| v.as_str()).unwrap_or("")
}

/// A glow or halo inside an `@media` block no 1280px viewport matches, on
/// an element the page shows, hides nothing: the cyan candidates report.
#[test]
fn pattern_candidates_in_inactive_at_rules_are_passed_over() {
let Some(engine) = engine() else { return };
let port = serve();
let findings = scan(&engine, port, "pattern-walk-inactive-at-rules.html");
let rows: Vec<(&str, &str, &str)> =
findings.iter().map(|f| (f.antipattern.as_str(), selector(f), f.snippet.as_str())).collect();
eprintln!("{rows:#?}");
for rule in ["dark-glow", "radial-halo", "codex-grid-background"] {
assert!(rows.iter().any(|(r, ..)| *r == rule), "{rule} reports: {rows:#?}");
}
assert!(!rows.iter().any(|(_, s, d)| s.contains("pass-") || d.contains("#ff00ff")), "{rows:#?}");
}

/// Code in a full-width page shell at `overflow: auto` that runs past the
/// screen edge is cut there; the same line in a 560px scroller is not.
#[test]
fn code_in_a_scrolling_page_shell_is_read() {
let Some(engine) = engine() else { return };
let port = serve();
let findings = scan(&engine, port, "text-overflow-code-page-shell.html");
let rows: Vec<&str> = findings.iter().filter(|f| f.antipattern == "text-overflow").map(selector).collect();
eprintln!("{rows:#?}");
assert!(rows.iter().any(|s| s.contains("flag-code-in-a-scrolling-shell")), "{rows:#?}");
assert!(!rows.iter().any(|s| s.contains("pass-")), "{rows:#?}");
}

/// The page's own part of the title, set huge and faint, is a heading to
/// read; the site's `og:site_name` set the same way is a wordmark.
#[test]
fn only_the_part_that_names_the_site_is_a_wordmark() {
let Some(engine) = engine() else { return };
let port = serve();
let findings = scan(&engine, port, "low-contrast-title-wordmark.html");
let low: Vec<(&str, &str)> = findings
.iter()
.filter(|f| f.antipattern == "low-contrast")
.map(|f| (selector(f), f.severity.as_str()))
.collect();
eprintln!("{low:#?}");
let severity = |id: &str| low.iter().find(|(s, _)| s.contains(id)).map(|(_, sev)| *sev);
assert_eq!(severity("flag-faint-page-name"), Some("warning"), "{low:#?}");
assert_eq!(severity("advisory-faint-site-name"), Some("advisory"), "{low:#?}");
}
74 changes: 62 additions & 12 deletions crates/core/src/browser/decorative_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,13 @@ fn shown_opacity(dom: &dyn Dom, el: ElId) -> f64 {
}

/// The names the site goes by: its host name run together and without its
/// top-level label (`screen.movie` gives `screenmovie` and `screen`), its
/// `og:site_name` or `application-name`, its title, and the title's first
/// and last parts between separators (`Incredible · AI that does tasks`
/// gives `Incredible`).
/// top-level label (`screen.movie` gives `screenmovie` and `screen`), and
/// its `og:site_name` or `application-name`. Where neither meta names it,
/// the title: whole when it has one part, and otherwise only the parts
/// between separators that the host name holds (`Incredible · AI that does
/// tasks` on incredible.ai gives `Incredible`). The other parts name a page
/// or a section (`Pricing | Acme`), and a faint display heading that
/// repeats one is not the site's wordmark.
fn site_names(dom: &dyn Dom) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
let host = js::to_lower_case(&dom.hostname());
Expand All @@ -178,29 +181,40 @@ fn site_names(dom: &dyn Dom) -> Vec<String> {
names.push(labels[..labels.len() - 1].concat());
}
}
let mut declared = false;
for selector in ["meta[property=\"og:site_name\"]", "meta[name=\"application-name\"]"] {
for meta in dom.query_all(None, selector).unwrap_or_default() {
if let Some(content) = dom.attr(meta, "content") {
if let Some(content) = dom.attr(meta, "content").filter(|c| !js::trim(c).is_empty()) {
names.push(content);
declared = true;
}
}
}
if declared {
return names;
}
if let Some(title) = dom.query_one(None, "title").ok().flatten() {
let title = collapsed_text(dom, title);
// The site's name opens or closes a title (`Incredible · AI that
// does tasks`, `Pricing | Acme`); a part in the middle is a page or
// section name. A hyphen joins words (`e-commerce`) and separates
// nothing here.
// A hyphen joins words (`e-commerce`) and separates nothing here.
let parts: Vec<String> = title
.split(['|', '·', '•', '—', '–', ':'])
.map(|part| js::trim(part).to_string())
.filter(|part| !part.is_empty())
.collect();
if parts.len() > 1 {
names.push(parts[0].clone());
names.push(parts[parts.len() - 1].clone());
let host_run = labels.concat();
let run = |text: &str| -> String {
text.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect()
};
for part in parts {
let joined = run(&part);
if joined.chars().count() >= 3 && host_run.contains(&joined) {
Comment thread
pbakaus marked this conversation as resolved.
Outdated
names.push(part);
}
Comment thread
pbakaus marked this conversation as resolved.
}
} else {
names.push(title);
}
names.push(title);
}
names
}
Expand Down Expand Up @@ -382,6 +396,42 @@ mod tests {
assert_eq!(decorative_text_shape_dom(&d, other), None);
}

/// #1076: a faint display heading that repeats the page's part of the
/// title (a 120px "Pricing" on "Pricing | Acme") is a page name, not
/// the site's wordmark. Only the part the host name or an explicit
/// site name corroborates names the site.
#[test]
fn a_title_part_names_the_site_only_where_something_backs_it() {
let build = |host: &str, site_name: Option<&str>, title_text: &str, text: &str| {
let mut d = FakeDom::new();
let (html, body) = d.with_page();
d.hostname = host.into();
let head = d.add(Some(html), "head");
let title = d.add(Some(head), "title");
d.add_text(title, title_text);
if let Some(name) = site_name {
let meta = d.add(Some(head), "meta");
d.add_selector(meta, "meta[property=\"og:site_name\"]");
d.set_attr(meta, "property", "og:site_name");
d.set_attr(meta, "content", name);
}
let mark = d.add(Some(body), "span");
d.add_text(mark, text);
d.set_styles(mark, &[("fontSize", "160px"), ("color", "rgba(0, 0, 0, 0.1)")]);
d.set_rect(mark, 0.0, 400.0, 900.0, 160.0);
decorative_text_shape_dom(&d, mark)
};
let wordmark = Some(DecorativeShape::Wordmark);
assert_eq!(build("", None, "Pricing | Acme", "Pricing"), None);
assert_eq!(build("", None, "Pricing | Acme", "Acme"), None, "nothing backs either part");
assert_eq!(build("getacme.io", None, "Pricing | Acme", "Acme"), wordmark);
assert_eq!(build("getacme.io", None, "Pricing | Acme", "Pricing"), None);
assert_eq!(build("", Some("Acme"), "Pricing | Acme", "Acme"), wordmark);
assert_eq!(build("", Some("Acme"), "Pricing | Acme", "Pricing"), None);
// A title of one part is the site's name, opened word for word.
assert_eq!(build("", None, "Screen Movie Blog", "Screen Movie"), wordmark);
}

#[test]
fn a_key_or_an_info_badge_is_not_initials() {
let mut d = FakeDom::new();
Expand Down
Loading
Loading