use assert_cmd::Command;
use predicates::prelude::*;
use std::io::Write;
use tempfile::{tempdir, NamedTempFile};
fn wcl_file(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".wcl").expect("tempfile");
f.write_all(content.as_bytes()).expect("write");
f.flush().expect("flush");
f
}
fn wcl(args: &[&str]) -> assert_cmd::assert::Assert {
Command::cargo_bin("wcl").unwrap().args(args).assert()
}
fn eval_json(content: &str) -> serde_json::Value {
let f = wcl_file(content);
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", f.path().to_str().unwrap()])
.output()
.expect("run wcl eval");
assert!(
output.status.success(),
"eval failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(stdout.trim()).expect("stdout should be valid JSON")
}
fn eval_expr_json(content: &str, expr: &str) -> serde_json::Value {
let f = wcl_file(content);
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", f.path().to_str().unwrap(), expr])
.output()
.expect("run wcl eval");
assert!(
output.status.success(),
"eval failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(stdout.trim()).expect("stdout should be valid JSON")
}
#[test]
fn eval_simple_attributes() {
let json = eval_json("name = \"hello\"\nport = 8080\nenabled = true\n");
assert_eq!(json["name"], "hello");
assert_eq!(json["port"], 8080);
assert_eq!(json["enabled"], true);
}
#[test]
fn eval_block_with_inline_id() {
let json = eval_json(
r#"
server web-prod {
host = "0.0.0.0"
port = 8080
}
"#,
);
assert_eq!(json["server"]["web-prod"]["host"], "0.0.0.0");
assert_eq!(json["server"]["web-prod"]["port"], 8080);
}
#[test]
fn eval_block_with_label() {
let json = eval_json(
r#"
server primary {
port = 443
}
"#,
);
assert_eq!(json["server"]["primary"]["port"], 443);
}
#[test]
fn eval_nested_blocks() {
let json = eval_json(
r#"
server web-prod {
port = 8080
logging {
level = "info"
}
}
"#,
);
assert_eq!(json["server"]["web-prod"]["logging"]["level"], "info");
}
#[test]
fn eval_direct_ref_preserves_id_in_json() {
let json = eval_json(
r#"
system wad {
name = "WAD"
}
app main {
system = wad
}
"#,
);
assert_eq!(json["app"]["main"]["system"]["id"], "wad");
assert_eq!(json["app"]["main"]["system"]["name"], "WAD");
}
#[test]
fn eval_let_bindings_not_in_output() {
let json = eval_json("let x = 42\nresult = x + 1\n");
assert!(json.get("x").is_none(), "let bindings should be erased");
assert_eq!(json["result"], 43);
}
#[test]
fn eval_arithmetic_expressions() {
let json = eval_json(
r#"
let base = 10
sum = base + 5
product = base * 3
div = base / 2
modulo = base % 3
neg = -base
"#,
);
assert_eq!(json["sum"], 15);
assert_eq!(json["product"], 30);
assert_eq!(json["div"], 5);
assert_eq!(json["modulo"], 1);
assert_eq!(json["neg"], -10);
}
#[test]
fn eval_string_interpolation() {
let json = eval_json(
r#"
let name = "world"
greeting = "hello, ${name}!"
"#,
);
assert_eq!(json["greeting"], "hello, world!");
}
#[test]
fn eval_ternary_expression() {
let json = eval_json(
r#"
let flag = true
result = flag ? "yes" : "no"
"#,
);
assert_eq!(json["result"], "yes");
}
#[test]
fn eval_list_and_map() {
let json = eval_json(
r#"
tags = ["a", "b", "c"]
meta = { x = 1, y = 2 }
"#,
);
assert_eq!(json["tags"], serde_json::json!(["a", "b", "c"]));
assert_eq!(json["meta"]["x"], 1);
assert_eq!(json["meta"]["y"], 2);
}
#[test]
fn eval_builtin_functions() {
let json = eval_json(
r#"
u = upper("hello")
l = lower("WORLD")
n = len([1, 2, 3])
m = max(5, 10)
s = sum([1, 2, 3, 4])
"#,
);
assert_eq!(json["u"], "HELLO");
assert_eq!(json["l"], "world");
assert_eq!(json["n"], 3);
assert_eq!(json["m"], 10);
assert_eq!(json["s"], 10);
}
#[test]
fn eval_higher_order_functions() {
let json = eval_json(
r#"
doubled = map([1, 2, 3], (x) => x * 2)
evens = filter([1, 2, 3, 4, 5, 6], (x) => x % 2 == 0)
total = reduce([1, 2, 3, 4], 0, (acc, x) => acc + x)
"#,
);
assert_eq!(json["doubled"], serde_json::json!([2, 4, 6]));
assert_eq!(json["evens"], serde_json::json!([2, 4, 6]));
assert_eq!(json["total"], 10);
}
#[test]
fn eval_user_defined_function() {
let json = eval_json(
r#"
let double = (x) => x * 2
result = double(21)
"#,
);
assert_eq!(json["result"], 42);
}
#[test]
fn eval_comparison_and_logic() {
let json = eval_json(
r#"
a = 5 > 3
b = 5 < 3
c = true && false
d = true || false
e = !true
"#,
);
assert_eq!(json["a"], true);
assert_eq!(json["b"], false);
assert_eq!(json["c"], false);
assert_eq!(json["d"], true);
assert_eq!(json["e"], false);
}
#[test]
fn eval_string_functions() {
let json = eval_json(
r#"
trimmed = trim(" hello ")
replaced = replace("foo bar foo", "foo", "baz")
joined = join(", ", ["a", "b", "c"])
sw = starts_with("hello world", "hello")
ew = ends_with("hello world", "world")
"#,
);
assert_eq!(json["trimmed"], "hello");
assert_eq!(json["replaced"], "baz bar baz");
assert_eq!(json["joined"], "a, b, c");
assert_eq!(json["sw"], true);
assert_eq!(json["ew"], true);
}
#[test]
fn eval_collection_functions() {
let json = eval_json(
r#"
sorted = sort([3, 1, 2])
reversed = reverse([1, 2, 3])
flat = flatten([[1, 2], [3, 4]])
merged = concat([1, 2], [3, 4])
unique = distinct([1, 2, 2, 3, 3, 3])
"#,
);
assert_eq!(json["sorted"], serde_json::json!([1, 2, 3]));
assert_eq!(json["reversed"], serde_json::json!([3, 2, 1]));
assert_eq!(json["flat"], serde_json::json!([1, 2, 3, 4]));
assert_eq!(json["merged"], serde_json::json!([1, 2, 3, 4]));
assert_eq!(json["unique"], serde_json::json!([1, 2, 3]));
}
#[test]
fn eval_for_loop_generates_attributes() {
let json = eval_json(
r#"
let items = [10, 20, 30]
total = sum(items)
count = len(items)
"#,
);
assert_eq!(json["total"], 60);
assert_eq!(json["count"], 3);
}
#[test]
fn eval_for_loop_expands_blocks() {
let json = eval_json(
r#"
let items = ["a", "b", "c"]
for item in items {
node ${item} {
name = item
}
}
"#,
);
assert!(json["node"].is_object());
assert_eq!(json["node"].as_object().unwrap().len(), 3);
assert_eq!(json["node"]["a"]["name"], "a");
}
#[test]
fn eval_if_else() {
let json = eval_json(
r#"
let debug = true
if debug {
log_level = "debug"
}
"#,
);
assert_eq!(json["log_level"], "debug");
}
#[test]
fn eval_if_else_false_branch() {
let json = eval_json(
r#"
let debug = false
if debug {
log_level = "debug"
} else {
log_level = "info"
}
"#,
);
assert_eq!(json["log_level"], "info");
}
#[test]
fn eval_default_format_is_wcl() {
let f = wcl_file("port = 8080\n");
wcl(&["eval", f.path().to_str().unwrap()])
.success()
.stdout(predicate::str::contains("port = 8080"));
}
#[test]
fn eval_unsupported_format_fails() {
let f = wcl_file("port = 8080\n");
wcl(&["eval", "--format", "yaml", f.path().to_str().unwrap()])
.failure()
.stderr(predicate::str::contains("unsupported format"));
}
#[test]
fn eval_expression_returns_value() {
let json = eval_expr_json("let services = [\"api\", \"web\"]\n", "services[0]");
assert_eq!(json, "api");
}
#[test]
fn eval_expression_with_function_projection() {
let json = eval_expr_json(
r#"
let nums = [1, 2, 3, 4]
let summarize = (xs) => { total = sum(xs), count = len(xs) }
"#,
"summarize(nums)",
);
assert_eq!(json["total"], 10);
assert_eq!(json["count"], 4);
}
#[test]
fn eval_expression_wcl_format() {
let f = wcl_file("let n = 42\n");
wcl(&["eval", f.path().to_str().unwrap(), "n + 1"])
.success()
.stdout(predicate::str::contains("43"));
}
#[test]
fn eval_invalid_file_fails() {
let f = wcl_file("server { port = \n");
wcl(&["eval", f.path().to_str().unwrap()])
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn eval_nonexistent_file_fails() {
wcl(&["eval", "/tmp/nonexistent_wcl_file_12345.wcl"])
.failure()
.stderr(predicate::str::contains("cannot read"));
}
#[test]
fn set_attribute_by_id_filter() {
let f = wcl_file(
r#"server svc-api {
port = 8080
host = "localhost"
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .id == \"svc-api\" ~> .port = 9090"])
.success()
.stdout(predicate::str::contains("set"));
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("9090"));
assert!(!updated.contains("8080"));
assert!(updated.contains("localhost"));
}
#[test]
fn set_string_value_by_id() {
let f = wcl_file(
r#"server svc-api {
host = "old.example.com"
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&[
"set",
&path,
"server | .id == \"svc-api\" ~> .host = \"new.example.com\"",
])
.success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("new.example.com"));
}
#[test]
fn set_creates_missing_attribute() {
let f = wcl_file("server web { port = 8080 }\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&[
"set",
&path,
"server | .id == \"web\" ~> .timeout = \"30s\"",
])
.success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("timeout"));
assert!(updated.contains("\"30s\""));
}
#[test]
fn set_no_match_fails() {
let f = wcl_file("server web { port = 8080 }\n");
wcl(&[
"set",
f.path().to_str().unwrap(),
"database | .id == \"db\" ~> .port = 5432",
])
.failure()
.stderr(predicate::str::contains("matched no blocks"));
}
#[test]
fn set_multi_match_updates_all() {
let f = wcl_file(
r#"server a { port = 8080 env = "prod" }
server b { port = 9090 env = "prod" }
server c { port = 7070 env = "dev" }
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .env == \"prod\" ~> .replicas = 4"]).success();
let updated = std::fs::read_to_string(&path).unwrap();
let count = updated.matches("replicas = 4").count();
assert_eq!(count, 2, "two prod servers should get replicas");
}
#[test]
fn set_preserves_other_content() {
let f = wcl_file(
r#"server svc-api {
host = "localhost"
port = 8080
debug = true
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .id == \"svc-api\" ~> .port = 9090"]).success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("localhost"));
assert!(updated.contains("debug"));
assert!(updated.contains("9090"));
}
#[test]
fn set_result_still_parses() {
let f = wcl_file(
r#"server svc-api {
port = 8080
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .id == \"svc-api\" ~> .port = 9090"]).success();
wcl(&["validate", &path]).success();
}
#[test]
fn add_top_level_attribute() {
let f = wcl_file("name = \"test\"\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["add", &path, "region = \"us-east\""])
.success()
.stdout(predicate::str::contains("added"));
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("region = \"us-east\""));
assert!(updated.contains("name = \"test\""));
}
#[test]
fn add_top_level_block() {
let f = wcl_file("name = \"test\"\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["add", &path, "server web {\n port = 8080\n}"]).success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("server web"));
assert!(updated.contains("port = 8080"));
}
#[test]
fn add_attribute_to_matched_block() {
let f = wcl_file("server api {\n port = 8080\n}\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&[
"add",
&path,
"server | .id == \"api\" ~> host = \"localhost\"",
])
.success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("host = \"localhost\""));
assert!(updated.contains("port = 8080"));
}
#[test]
fn add_child_block_to_matched_block() {
let f = wcl_file("server api {\n port = 8080\n}\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&[
"add",
&path,
"server | .id == \"api\" ~> tls { enabled = true }",
])
.success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("tls {"));
assert!(updated.contains("enabled = true"));
}
#[test]
fn add_invalid_fragment_fails() {
let f = wcl_file("server api {\n port = 8080\n}\n");
wcl(&[
"add",
f.path().to_str().unwrap(),
"this is not valid wcl @@@",
])
.failure()
.stderr(predicate::str::contains("invalid WCL fragment").or(predicate::str::contains("error")));
}
#[test]
fn add_result_still_parses() {
let f = wcl_file("server api {\n port = 8080\n}\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&[
"add",
&path,
"server | .id == \"api\" ~> host = \"localhost\"",
])
.success();
wcl(&["validate", &path]).success();
}
#[test]
fn add_to_invalid_file_fails() {
let f = wcl_file("server { port = \n");
wcl(&["add", f.path().to_str().unwrap(), "x = 1"])
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn remove_block_by_id() {
let f = wcl_file(
r#"server svc-api {
port = 8080
}
server svc-old {
port = 3000
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .id == \"svc-old\""])
.success()
.stdout(predicate::str::contains("removed"));
let updated = std::fs::read_to_string(&path).unwrap();
assert!(!updated.contains("svc-old"));
assert!(updated.contains("svc-api"));
assert!(updated.contains("8080"));
}
#[test]
fn remove_attribute_from_block() {
let f = wcl_file(
r#"server svc-api {
port = 8080
debug = true
host = "localhost"
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .id == \"svc-api\" ~> .debug"])
.success()
.stdout(predicate::str::contains("removed"));
let updated = std::fs::read_to_string(&path).unwrap();
assert!(!updated.contains("debug"));
assert!(updated.contains("port"));
assert!(updated.contains("host"));
}
#[test]
fn remove_multi_match_block() {
let f = wcl_file(
r#"server a { env = "dev" }
server b { env = "prod" }
server c { env = "dev" }
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .env == \"dev\""]).success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(!updated.contains("server a"));
assert!(!updated.contains("server c"));
assert!(updated.contains("server b"));
}
#[test]
fn remove_result_still_parses() {
let f = wcl_file(
r#"server svc-api {
port = 8080
debug = true
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .id == \"svc-api\" ~> .debug"]).success();
wcl(&["validate", &path]).success();
}
#[test]
fn remove_no_match_fails() {
let f = wcl_file("server svc-api { port = 8080 }\n");
wcl(&[
"remove",
f.path().to_str().unwrap(),
"server | .id == \"missing\"",
])
.failure()
.stderr(predicate::str::contains("matched no blocks"));
}
#[test]
fn set_then_eval_reflects_change() {
let f = wcl_file(
r#"server svc-api {
port = 8080
host = "localhost"
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .id == \"svc-api\" ~> .port = 9090"]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert_eq!(json["server"]["svc-api"]["port"], 9090);
assert_eq!(json["server"]["svc-api"]["host"], "localhost");
}
#[test]
fn add_then_eval_shows_new_block() {
let f = wcl_file("server svc-api {\n port = 8080\n}\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["add", &path, "server svc-new {\n port = 7070\n}"]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(
json["server"]["svc-api"].is_object(),
"original block should exist"
);
assert!(
json["server"]["svc-new"].is_object(),
"new block should exist"
);
}
#[test]
fn remove_then_eval_block_gone() {
let f = wcl_file(
r#"server svc-api {
port = 8080
}
server svc-old {
port = 3000
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .id == \"svc-old\""]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(
json["server"]["svc-api"].is_object(),
"kept block should exist"
);
assert!(
json["server"]["svc-old"].is_null(),
"removed block should be gone"
);
}
#[test]
fn remove_attr_then_eval_attr_gone() {
let f = wcl_file(
r#"server svc-api {
port = 8080
debug = true
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["remove", &path, "server | .id == \"svc-api\" ~> .debug"]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert_eq!(json["server"]["svc-api"]["port"], 8080);
assert!(
json["server"]["svc-api"]["debug"].is_null(),
"debug should be gone"
);
}
#[test]
fn validate_with_schema_valid() {
let f = wcl_file(
r#"
schema "server" {
port: i64
host: string
}
server web {
port = 8080
host = "localhost"
}
"#,
);
wcl(&["validate", f.path().to_str().unwrap()]).success();
}
#[test]
fn validate_with_schema_type_mismatch() {
let f = wcl_file(
r#"
schema "server" {
port: i64
}
server web {
port = "not_a_number"
}
"#,
);
wcl(&["validate", f.path().to_str().unwrap()])
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn validate_with_schema_missing_required() {
let f = wcl_file(
r#"
schema "server" {
port: i64
host: string
}
server web {
port = 8080
}
"#,
);
wcl(&["validate", f.path().to_str().unwrap()])
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn validate_schema_optional_field() {
let f = wcl_file(
r#"
schema "server" {
port: i64
host: string @optional
}
server web {
port = 8080
}
"#,
);
wcl(&["validate", f.path().to_str().unwrap()]).success();
}
#[test]
fn fmt_outputs_to_stdout() {
let f = wcl_file(" x = 1 \n");
wcl(&["fmt", f.path().to_str().unwrap()])
.success()
.stdout(predicate::str::contains("x = 1"));
}
#[test]
fn fmt_write_modifies_file() {
let f = wcl_file(" x = 1 \n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["fmt", "--write", &path]).success();
let updated = std::fs::read_to_string(&path).unwrap();
assert!(updated.contains("x = 1"), "file should be formatted");
}
#[test]
fn fmt_check_returns_success_when_formatted() {
let f = wcl_file("x = 1\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["fmt", "--write", &path]).success();
wcl(&["fmt", "--check", &path]).success();
}
#[test]
fn wdoc_build_renders_drawing_image_and_copies_asset() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
let images_dir = pages_dir.join("images/deep");
std::fs::create_dir_all(&images_dir).expect("create images dir");
std::fs::write(images_dir.join("hero.png"), [0x89, b'P', b'N', b'G']).expect("write image");
let site = r#"
import <wdoc.wcl>
import "./pages/page.wcl"
use wdoc::{doc, section}
doc my_docs {
title = "Image Docs"
section overview "Overview" {}
}
"#;
let page = r#"
import <wdoc.wcl>
use wdoc::{page, layout}
use wdoc::draw::{diagram, image}
page home {
section = "my_docs.overview"
title = "Home"
layout {
diagram hero_diagram {
width = 200
height = 120
image hero {
x = 20
y = 10
width = 160
height = 90
src = "images/deep/hero.png"
alt = "Hero image"
fit = "cover"
rx = 6
ry = 6
}
}
}
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write wdoc file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
assert!(html.contains("<image href=\"images/deep/hero.png\""));
assert!(html.contains("preserveAspectRatio=\"xMidYMid slice\""));
assert!(html.contains("role=\"img\" aria-label=\"Hero image\""));
assert!(output.join("images/deep/hero.png").exists());
}
#[test]
fn wdoc_build_embeds_local_inline_svg_from_import_dir() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
let icons_dir = pages_dir.join("icons");
std::fs::create_dir_all(&icons_dir).expect("create icons dir");
std::fs::write(
icons_dir.join("logo.svg"),
r#"<svg viewBox="0 0 10 10"><script>alert(1)</script><path class="logo-path" onclick="bad()" d="M0 0L10 10"/></svg>"#,
)
.expect("write svg");
let site = r#"
import <wdoc.wcl>
import "./pages/page.wcl"
use wdoc::{doc, section}
doc my_docs {
title = "Inline SVG Docs"
section overview "Overview" {}
}
"#;
let page = r#"
import <wdoc.wcl>
use wdoc::{page, layout}
use wdoc::draw::{diagram, inline_svg}
page home {
section = "my_docs.overview"
title = "Home"
layout {
diagram logo_diagram {
width = 80
height = 80
inline_svg logo {
x = 10
y = 12
width = 40
height = 40
src = "icons/logo.svg"
class = "brand-logo"
fill = "currentColor"
}
}
}
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write wdoc file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
assert!(html.contains("class=\"brand-logo\""));
assert!(html.contains("<path class=\"logo-path\" d=\"M0 0L10 10\""));
assert!(html.contains("fill=\"currentColor\""));
assert!(!html.contains("alert(1)"));
assert!(!html.contains("onclick"));
}
#[test]
fn wdoc_build_registers_design_system_diagram_css_once() {
let dir = tempdir().expect("tempdir");
let site = r#"
import <wdoc.wcl>
use wdoc::{doc, section, page, layout}
use wdoc::draw::{diagram, rect}
doc my_docs {
title = "Design Docs"
section overview "Overview" {}
}
page home {
section = "my_docs.overview"
title = "Home"
layout {
diagram first {
width = 80
height = 40
design_system = "wad_interface"
css = ".ui-button-bg { fill: red; }"
rect bg {
x = 0
y = 0
width = 40
height = 20
class = "ui-button-bg"
}
}
diagram second {
width = 80
height = 40
design_system = "wad_interface"
css = ".ui-button-bg { fill: red; }"
rect bg {
x = 0
y = 0
width = 40
height = 20
class = "ui-button-bg"
}
}
}
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write wdoc file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
let css = std::fs::read_to_string(output.join("styles.css")).expect("read styles");
assert_eq!(html.matches("class=\"wad-ds-wad_interface\"").count(), 2);
assert!(!html.contains("<style>"));
assert_eq!(
css.matches(".wad-ds-wad_interface .ui-button-bg").count(),
1
);
}
#[test]
fn wdoc_build_registers_imported_css_fragment_once() {
let dir = tempdir().expect("tempdir");
let fragments_dir = dir.path().join("fragments");
std::fs::create_dir_all(&fragments_dir).expect("create fragments dir");
let site = r#"
import <wdoc.wcl>
import "./fragments/css.wcl"
use wdoc::{doc, section, page, layout}
use wdoc::draw::{diagram, rect}
doc my_docs {
title = "Design Docs"
section overview "Overview" {}
}
page home {
section = "my_docs.overview"
title = "Home"
layout {
diagram first {
width = 80
height = 40
design_system = "wad_interface"
rect bg {
x = 0
y = 0
width = 40
height = 20
class = "token-swatch"
}
}
diagram second {
width = 80
height = 40
design_system = "wad_interface"
rect bg {
x = 0
y = 0
width = 40
height = 20
class = "token-swatch"
}
}
}
}
"#;
let css_fragment = r#"
import <wdoc.wcl>
use wdoc::{css_fragment}
css_fragment wad_interface_tokens {
scope = "wad_interface"
css = ".token-swatch { fill: var(--wad-token-frost); }"
}
css_fragment wad_interface_tokens_duplicate {
scope = "wad_interface"
css = ".token-swatch { fill: var(--wad-token-frost); }"
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write wdoc file");
std::fs::write(fragments_dir.join("css.wcl"), css_fragment).expect("write fragment file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
let css = std::fs::read_to_string(output.join("styles.css")).expect("read styles");
assert_eq!(html.matches("class=\"wad-ds-wad_interface\"").count(), 2);
assert!(!html.contains("<style>"));
assert_eq!(
css.matches(".wad-ds-wad_interface .token-swatch").count(),
1
);
}
#[test]
fn wdoc_build_registers_imported_font_asset_and_global_css() {
let dir = tempdir().expect("tempdir");
let theme_dir = dir.path().join("theme");
std::fs::create_dir_all(theme_dir.join("fonts")).expect("create theme font dir");
std::fs::write(theme_dir.join("fonts/Inter-Regular.woff2"), [0, 1, 2, 3]).expect("write font");
let site = r#"
import <wdoc.wcl>
import "./theme/fonts.wcl"
use wdoc::{doc, section, page, layout}
use wdoc::draw::{diagram, text}
doc my_docs {
title = "Design Docs"
section overview "Overview" {}
}
page home {
section = "my_docs.overview"
title = "Home"
layout {
diagram first {
width = 160
height = 60
text label {
x = 8
y = 24
content = "Hello"
font_family = "\"Inter\", system-ui, sans-serif"
font_size = 14
}
}
}
}
"#;
let fonts = r#"
import <wdoc.wcl>
use wdoc::{font_asset, global_css}
font_asset inter_regular {
family = "Inter"
src = "fonts/Inter-Regular.woff2"
weight = "400"
style = "normal"
display = "swap"
}
font_asset inter_regular_duplicate {
family = "Inter"
src = "fonts/Inter-Regular.woff2"
weight = "400"
style = "normal"
display = "swap"
}
global_css app_fonts {
css = ":root { --font-body: \"Inter\", system-ui, sans-serif; }"
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write wdoc file");
std::fs::write(theme_dir.join("fonts.wcl"), fonts).expect("write fonts file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
let css = std::fs::read_to_string(output.join("styles.css")).expect("read styles");
assert_eq!(css.matches("font-family: \"Inter\";").count(), 1);
assert!(css.contains("font-family: \"Inter\";"));
assert!(css.contains("src: url(\"fonts/Inter-Regular.woff2\") format(\"woff2\");"));
assert!(css.contains(":root { --font-body: \"Inter\", system-ui, sans-serif; }"));
assert!(!css.contains(".wad-ds-wad_interface :root"));
assert!(html.contains("font-family=\""Inter", system-ui, sans-serif\""));
assert!(output.join("fonts/Inter-Regular.woff2").exists());
}
#[test]
fn wdoc_build_expands_imported_macro_inside_diagram_body() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
std::fs::create_dir_all(&pages_dir).expect("create pages dir");
let site = r#"
import <wdoc.wcl>
import "./pages/page.wcl"
use wdoc::{doc, section}
doc my_docs {
title = "Macro Docs"
section overview "Overview" {}
}
"#;
let renderers = r##"
import <wdoc.wcl>
use wdoc::draw::{group, rect, text}
export macro action_button(label_text, xpos) {
group button {
x = xpos
y = 20
width = 120
height = 36
rect bg {
x = 0
y = 0
width = 120
height = 36
rx = 6
fill = "#5E81AC"
}
text caption {
x = 0
y = 0
width = 120
height = 36
content = label_text
fill = "#fff"
}
}
}
"##;
let page = r#"
import <wdoc.wcl>
import "./renderers.wcl"
use wdoc::{page, layout}
use wdoc::draw::{diagram}
page home {
section = "my_docs.overview"
title = "Home"
layout {
let button_text = "Preview"
diagram macro_diagram {
width = 180
height = 80
action_button(button_text, 30)
}
}
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write site file");
std::fs::write(pages_dir.join("renderers.wcl"), renderers).expect("write renderer file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
assert!(html.contains("<g transform=\"translate(30,20)\""));
assert!(html.contains(">Preview</text>"));
assert!(html.contains("fill=\"#5E81AC\""));
}
#[test]
fn wdoc_build_expands_imported_partial_macros_inside_diagram_body() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
std::fs::create_dir_all(&pages_dir).expect("create pages dir");
let site = r#"
import <wdoc.wcl>
import "./pages/page.wcl"
use wdoc::{doc, section}
doc my_docs {
title = "Partial Macro Docs"
section overview "Overview" {}
}
"#;
let renderer_a = r##"
import <wdoc.wcl>
use wdoc::draw::{rect, text}
export partial macro render_example(label_text) {
if label_text == "Preview" {
rect first {
x = 10
y = 12
width = 70
height = 30
fill = "#BF616A"
}
text first_label {
x = 16
y = 18
width = 60
height = 20
content = label_text
fill = "#fff"
}
}
}
"##;
let renderer_b = r##"
import <wdoc.wcl>
use wdoc::draw::{rect, text}
export partial macro render_example(label_text) {
rect second {
x = 90
y = 12
width = 70
height = 30
fill = "#A3BE8C"
}
text second_label {
x = 96
y = 18
width = 60
height = 20
content = label_text
fill = "#111"
}
}
"##;
let page = r#"
import <wdoc.wcl>
import "./renderer_a.wcl"
import "./renderer_b.wcl"
use wdoc::{page, layout}
use wdoc::draw::{diagram}
page home {
section = "my_docs.overview"
title = "Home"
layout {
let button_text = "Preview"
diagram macro_diagram {
width = 180
height = 70
render_example(button_text)
}
}
}
"#;
let input = dir.path().join("site.wcl");
std::fs::write(&input, site).expect("write site file");
std::fs::write(pages_dir.join("renderer_a.wcl"), renderer_a).expect("write renderer a file");
std::fs::write(pages_dir.join("renderer_b.wcl"), renderer_b).expect("write renderer b file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("home.html")).expect("read rendered page");
let first = html.find("fill=\"#BF616A\"").expect("first fragment");
let second = html.find("fill=\"#A3BE8C\"").expect("second fragment");
assert!(first < second);
assert!(html.matches(">Preview</text>").count() >= 2);
}
#[test]
fn wdoc_build_binds_for_iterator_args_for_imported_partial_macro_body_loops() {
let dir = tempdir().expect("tempdir");
let site = r#"
import <wdoc.wcl>
use wdoc::{doc, page, section, layout}
use wdoc::draw::{diagram, rect}
import "./page.wcl"
import "./control.wcl"
let components = [{ id = "a" }]
let items = [{ id = "i", component = "a" }]
doc d {
title = "D"
section s "S" {}
}
"#;
let page = r#"
for component in components {
page p-${component.id} {
section = "d.s"
title = "P"
layout {
diagram demo {
width = 50
height = 50
render(component, items)
}
}
}
}
"#;
let control = r#"
partial macro render(component, items) {
for item in filter(items, item => item.component == component.id) {
rect r-${item.id} {
x = 1
y = 1
width = 10
height = 10
fill = "red"
}
}
}
"#;
let input = dir.path().join("main.wcl");
std::fs::write(&input, site).expect("write site file");
std::fs::write(dir.path().join("page.wcl"), page).expect("write page file");
std::fs::write(dir.path().join("control.wcl"), control).expect("write control file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("p-a.html")).expect("read rendered page");
assert!(html.contains("<rect"));
assert!(html.contains("fill=\"red\""));
}
#[test]
fn wdoc_build_keeps_partial_macro_params_bound_in_globbed_nested_documents() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
let controls_dir = dir.path().join("controls");
std::fs::create_dir_all(&pages_dir).expect("create pages dir");
std::fs::create_dir_all(&controls_dir).expect("create controls dir");
let site = r#"
import <wdoc.wcl>
use wdoc::{doc, page, section, layout}
use wdoc::draw::{diagram, rect, text}
schema "before_import" { name: string }
before_import alpha { name = "before" }
import "./pages/page.wcl"
import "./controls/*.wcl"
let components = [{ id = "ds_button", label = "Button" }]
let example_preview_height = 50
let ui_component_example_element_models = [
{ id = "first", component = "ds_button", x = 1 },
{ id = "skip", component = "other", x = 20 },
]
schema "after_import" { name: string }
after_import omega { name = "after" }
doc d {
title = "D"
section s "S" {}
}
"#;
let page = r#"
for component in components {
page p-${component.id} {
section = "d.s"
title = "P"
layout {
diagram demo {
width = 90
height = example_preview_height
wad_render_ui_component_examples(
component,
example_preview_height,
ui_component_example_element_models
)
}
}
}
}
"#;
let control = r#"
partial macro wad_render_ui_component_examples(component, example_preview_height, ui_component_example_element_models) {
if to_string(component.id) == "ds_button" {
let labels = map(ui_component_example_element_models, item => item.id)
for example_element in filter(
ui_component_example_element_models,
item => item.component == to_string(component.id)
) {
rect example-${component.id}-${example_element.id} {
x = example_element.x
y = 1
width = 10
height = example_preview_height - 40
fill = "red"
}
text label-${example_element.id} {
x = 1
y = 20
content = to_string(component.label) + " " + to_string(labels[0])
font_size = 10
}
}
}
}
"#;
let input = dir.path().join("main.wcl");
std::fs::write(&input, site).expect("write site file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
std::fs::write(controls_dir.join("button.wcl"), control).expect("write control file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("p-ds_button.html")).expect("read page");
assert!(html.contains("<rect"));
assert!(html.contains("fill=\"red\""));
assert!(html.contains(">Button first</text>"));
}
#[test]
fn wdoc_build_preserves_block_ref_partial_macro_args_from_query_loops() {
let dir = tempdir().expect("tempdir");
let pages_dir = dir.path().join("pages");
let controls_dir = dir.path().join("controls");
std::fs::create_dir_all(&pages_dir).expect("create pages dir");
std::fs::create_dir_all(&controls_dir).expect("create controls dir");
let site = r#"
import <wdoc.wcl>
use wdoc::{doc, page, section, layout}
use wdoc::draw::{diagram, rect, text}
import "./pages/page.wcl"
import "./controls/*.wcl"
schema "UiComponent" {
label: string
}
UiComponent ds_button {
label = "Button"
}
let example_preview_height = 50
let ui_component_example_element_models = [{ id = "first", component = "ds_button" }]
doc d {
title = "D"
section s "S" {}
}
"#;
let page = r#"
for component in (..UiComponent) {
page p-${component.id} {
section = "d.s"
title = "P"
layout {
diagram demo {
width = 90
height = example_preview_height
wad_render_ui_component_examples(
component,
example_preview_height,
ui_component_example_element_models
)
}
}
}
}
"#;
let control = r#"
partial macro wad_render_ui_component_examples(component, example_preview_height, ui_component_example_element_models) {
if to_string(component.id) == "ds_button" {
for example_element in filter(
ui_component_example_element_models,
item => item.component == to_string(component.id)
) {
rect r-${example_element.id} {
x = 1
y = 1
width = 10
height = example_preview_height - 40
fill = "red"
}
text label-${example_element.id} {
x = 1
y = 20
content = component.label
font_size = 10
}
}
}
}
"#;
let input = dir.path().join("main.wcl");
std::fs::write(&input, site).expect("write site file");
std::fs::write(pages_dir.join("page.wcl"), page).expect("write page file");
std::fs::write(controls_dir.join("button.wcl"), control).expect("write control file");
let output = dir.path().join("out");
Command::cargo_bin("wcl")
.unwrap()
.args([
"wdoc",
"build",
input.to_str().unwrap(),
"--output",
output.to_str().unwrap(),
])
.assert()
.success();
let html = std::fs::read_to_string(output.join("p-ds_button.html")).expect("read page");
assert!(html.contains("fill=\"red\""));
assert!(html.contains(">Button</text>"));
}
#[test]
fn workflow_add_set_eval() {
let f = wcl_file("server svc-api {\n port = 8080\n}\n");
let path = f.path().to_str().unwrap().to_string();
wcl(&["add", &path, "server svc-worker {\n port = 3000\n}"]).success();
wcl(&["validate", &path]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert_eq!(json["server"]["svc-api"]["port"], 8080);
assert_eq!(json["server"]["svc-worker"]["port"], 3000);
}
#[test]
fn workflow_set_validate_eval() {
let f = wcl_file(
r#"
schema "server" {
port: i64
host: string @optional
}
server svc-api {
port = 8080
host = "localhost"
}
"#,
);
let path = f.path().to_str().unwrap().to_string();
wcl(&["set", &path, "server | .id == \"svc-api\" ~> .port = 9090"]).success();
wcl(&["validate", &path]).success();
let output = Command::cargo_bin("wcl")
.unwrap()
.args(["eval", "--format", "json", &path])
.output()
.expect("run eval");
assert!(output.status.success());
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert_eq!(json["server"]["svc-api"]["port"], 9090);
}