From 3f345f70589a0fdd8c24578eee6bef9e474770f3 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Fri, 11 Sep 2026 12:24:57 -0700 Subject: [PATCH] Add a real-browser regression for the documented-palette case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile asked for coverage beyond FakeDom, and it is right that the interesting question here is a browser one: whether the `oklch()` a DESIGN.md declares and the `oklch()` Chrome computes for an element are the same color by the time the rule sees them. A `file://` target loads the DESIGN.md that governs the page's directory, so the whole path runs end to end: the allowlist is parsed from markdown, Chrome renders the page, and the browser element sweep decides. The test writes two pages with the same shapes — verdigris text on a dark instrument face, and a violet-to-verdigris gradient — one in declared tokens and one in colors the DESIGN.md never mentions, and asserts the first is silent while the second still fires. It skips cleanly with no installed browser or no built binary, the way differential.rs does. Verified against the pre-fix engine: the declared page reports two ai-color-palette findings there and none here. Assisted-by: Claude Code Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL --- crates/browser/tests/design_system_palette.rs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 crates/browser/tests/design_system_palette.rs diff --git a/crates/browser/tests/design_system_palette.rs b/crates/browser/tests/design_system_palette.rs new file mode 100644 index 000000000..d1088841f --- /dev/null +++ b/crates/browser/tests/design_system_palette.rs @@ -0,0 +1,138 @@ +//! `ai-color-palette` against a project's own documented palette, through a +//! real browser. +//! +//! The FakeDom tests in `impeccable-core` pin the rule's decision; this one +//! pins the thing only a browser can answer: that the `oklch()` a DESIGN.md +//! declares and the `oklch()` Chrome computes for an element are the same +//! color by the time the rule sees them. A `file://` target loads the +//! DESIGN.md that governs the page's directory, so the whole path runs — the +//! allowlist is parsed from the markdown, the page is rendered, and the +//! browser element sweep decides. +//! +//! Skips cleanly without an installed browser or a built binary, the way +//! `differential.rs` does. +//! +//! Env: +//! - `IMPECCABLE_BIN` — the binary (default `target/debug/impeccable`). + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde_json::Value; + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("workspace root") +} + +/// A DESIGN.md whose palette is all `oklch()`, including a verdigris that +/// sits inside the rule's cyan band and a violet inside its purple one. +const DESIGN_MD: &str = "---\n\ +name: Instruments\n\ +colors:\n\ +\x20 paper: \"oklch(97.8% 0 0)\"\n\ +\x20 ink: \"oklch(13% 0 0)\"\n\ +\x20 instrument: \"oklch(24% 0 0)\"\n\ +\x20 patina: \"oklch(70% 0.12 188)\"\n\ +\x20 iris: \"oklch(58% 0.2 300)\"\n\ +---\n\n\ +The palette above is the whole system.\n"; + +fn page(swatch_color: &str, gradient: &str) -> String { + format!( + "\ +Instruments\ +\ +
\ +Live\ +
\ +
\ +" + ) +} + +fn rules(bin: &Path, dir: &Path, file: &str) -> Vec { + let url = format!("file://{}", dir.join(file).display()); + let out = Command::new(bin) + .arg("detect") + .arg("--json") + .arg(&url) + .current_dir(dir) + .output() + .expect("run detect"); + let stdout = String::from_utf8_lossy(&out.stdout).to_string(); + let parsed: Value = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("detect {url} did not print JSON ({e}): {stdout}")); + parsed + .as_array() + .expect("findings array") + .iter() + .filter_map(|f| f.get("antipattern")?.as_str().map(str::to_string)) + .collect() +} + +#[test] +fn ai_palette_respects_a_documented_oklch_palette() { + let env: HashMap = std::env::vars().collect(); + if impeccable_browser::discovery::find_browser(&env).is_err() { + eprintln!("skip: no installed browser found"); + return; + } + let bin = std::env::var("IMPECCABLE_BIN") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root().join("target/debug/impeccable")); + if !bin.exists() { + eprintln!( + "skip: {} missing (cargo build -p impeccable, or set IMPECCABLE_BIN)", + bin.display() + ); + return; + } + + let dir = std::env::temp_dir().join(format!("impeccable-ds-palette-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("temp dir"); + // A project marker, so the DESIGN.md walk-up stops here rather than + // climbing out of the temp directory. + std::fs::write(dir.join("package.json"), "{\"name\":\"ds-palette-fixture\"}\n").unwrap(); + std::fs::write(dir.join("DESIGN.md"), DESIGN_MD).unwrap(); + std::fs::write( + dir.join("declared.html"), + page( + "oklch(70% 0.12 188)", + "linear-gradient(oklch(58% 0.2 300), oklch(70% 0.12 188))", + ), + ) + .unwrap(); + std::fs::write( + dir.join("undeclared.html"), + page( + "rgb(0, 229, 255)", + "linear-gradient(rgb(168, 85, 247), rgb(59, 130, 246))", + ), + ) + .unwrap(); + + let declared = rules(&bin, &dir, "declared.html"); + let undeclared = rules(&bin, &dir, "undeclared.html"); + let _ = std::fs::remove_dir_all(&dir); + + // Every color on this page is a token the DESIGN.md declares, so neither + // the palette rule nor the drift rule has anything to say. + assert!( + !declared.iter().any(|r| r == "ai-color-palette"), + "declared tokens reported as a generic AI palette: {declared:?}" + ); + assert!( + !declared.iter().any(|r| r == "design-system-color"), + "declared tokens reported as drift: {declared:?}" + ); + + // The same shapes in colors the DESIGN.md never declared still fire. + assert!( + undeclared.iter().any(|r| r == "ai-color-palette"), + "undeclared neon and violet gradient went unreported: {undeclared:?}" + ); +}