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:?}" + ); +}