Files
isokit/crates/isokit-project/tests/demo.rs
T

268 lines
8.7 KiB
Rust

//! Integration tests on the demo project: load, render every view, export,
//! check, document operations through a Workspace, MCP round trips.
use isokit_project::mcp;
use isokit_project::ops::Op;
use isokit_project::workspace::Workspace;
use isokit_project::{check, page, Project};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
fn demo() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/demo")
}
/// A throwaway copy of the demo (tests edit it).
fn copy_demo(name: &str) -> PathBuf {
let dst = std::env::temp_dir().join(format!("isokit-test-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dst);
fn cp(a: &Path, b: &Path) {
std::fs::create_dir_all(b).unwrap();
for e in std::fs::read_dir(a).unwrap().flatten() {
let p = e.path();
let n = e.file_name();
if p.is_dir() {
if n != "dist" {
cp(&p, &b.join(n));
}
} else {
std::fs::copy(&p, b.join(n)).unwrap();
}
}
}
cp(&demo(), &dst);
dst
}
#[test]
fn demo_renders_every_view_cleanly() {
let p = Project::load(demo()).unwrap();
assert!(p.runtime.is_ok(), "{:?}", p.runtime.as_ref().err());
for l in p.manifest.levels() {
for rot in 0..4 {
let v = p.render(&l.id, rot, None, false).unwrap();
assert!(v.warnings.is_empty(), "{}/{rot}: {:?}", l.id, v.warnings);
assert!(!v.html.contains("NaN"));
assert!(
v.nodes < p.manifest.build.budget,
"{}/{rot}: {} nodes",
l.id,
v.nodes
);
for o in &p.doc.objects {
assert!(
v.boxes.contains_key(&o.id),
"{} not drawn in {}/{rot}",
o.id,
l.id
);
assert!(v.info.contains_key(&o.id), "{} has no tooltip", o.id);
}
// the script's sign and the project prefab are there
assert!(v.html.contains("data-id=\"sign\""));
assert!(v.html.contains("data-obj=\"kiosk\""));
}
}
}
#[test]
fn demo_passes_the_check() {
let p = Project::load(demo()).unwrap();
let f = check::check(&p);
assert!(f.is_empty(), "{f:?}");
}
#[test]
fn static_page_and_svg_export() {
let p = Project::load(demo()).unwrap();
let html = page::static_page(&p).unwrap();
assert!(
html.contains("IsoKit.mount") && html.contains("\"site/1\"") && html.contains("\"gf/3\"")
);
assert!(
!html.contains("/*ISOKIT_"),
"a placeholder was left in the page"
);
assert!(
html.contains(".iso-brick { --b: var(--iso-brick); }"),
"project theme merged"
);
let svg = page::svg(&p, "site", 1, true).unwrap();
assert!(
svg.starts_with("<svg") && svg.contains("<style>") && svg.contains("iso-wall")
|| svg.contains("iso-render")
);
}
#[test]
fn workspace_edits_save_and_undo() {
let dir = copy_demo("ws");
let mut ws = Workspace::open(&dir).unwrap();
let n = ws.project.doc.objects.len();
let id = ws
.apply(
serde_json::from_value::<Op>(json!({"op": "add", "type": "tree", "x": 1, "y": 2}))
.unwrap(),
)
.unwrap()
.id
.unwrap();
// saved to disk
let on_disk = Project::load(&dir).unwrap();
assert_eq!(on_disk.doc.objects.len(), n + 1);
assert!(on_disk.doc.get(&id).is_some());
ws.undo().unwrap();
assert_eq!(Project::load(&dir).unwrap().doc.objects.len(), n);
ws.redo().unwrap();
assert_eq!(ws.project.doc.objects.len(), n + 1);
// an outside edit (another process) is picked up and can be undone
let mut d = on_disk.doc.clone();
d.objects.retain(|o| o.kind != "lamp");
std::fs::write(
dir.join("scene.json"),
serde_json::to_string_pretty(&d).unwrap(),
)
.unwrap();
assert!(ws.refresh());
assert!(ws.project.doc.objects.iter().all(|o| o.kind != "lamp"));
ws.undo().unwrap();
assert!(ws.project.doc.objects.iter().any(|o| o.kind == "lamp"));
let _ = std::fs::remove_dir_all(&dir);
}
fn call(ws: &mut Workspace, id: u64, name: &str, args: Value) -> (bool, Value) {
let r = mcp::handle(ws, &json!({"jsonrpc": "2.0", "id": id, "method": "tools/call", "params": {"name": name, "arguments": args}})).unwrap();
let res = &r["result"];
let text = res["content"][0]["text"]
.as_str()
.unwrap_or_default()
.to_string();
(
res["isError"].as_bool().unwrap_or(true),
serde_json::from_str(&text).unwrap_or(Value::String(text)),
)
}
#[test]
fn mcp_round_trip() {
let dir = copy_demo("mcp");
let mut ws = Workspace::open(&dir).unwrap();
let init = mcp::handle(&mut ws, &json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}})).unwrap();
assert_eq!(init["result"]["protocolVersion"], "2025-06-18");
assert!(mcp::handle(
&mut ws,
&json!({"jsonrpc": "2.0", "method": "notifications/initialized"})
)
.is_none());
let list = mcp::handle(
&mut ws,
&json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
)
.unwrap();
let names: Vec<&str> = list["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
for t in [
"project_info",
"list_prefabs",
"add_object",
"update_object",
"render_view",
"check_project",
"build_html",
] {
assert!(names.contains(&t), "{t}");
}
let (err, info) = call(&mut ws, 3, "project_info", json!({}));
assert!(!err && info["objects"].as_u64().unwrap() > 10);
let (err, pf) = call(&mut ws, 4, "list_prefabs", json!({"type": "building"}));
assert!(
!err && pf["props"]
.as_array()
.unwrap()
.iter()
.any(|p| p["key"] == "floors")
);
let (err, added) = call(
&mut ws,
5,
"add_object",
json!({"type": "building", "x": 20, "y": 15, "props": {"w": 8, "d": 6, "floors": 1}, "name": "Store"}),
);
assert!(!err, "{added}");
let id = added["id"].as_str().unwrap().to_string();
let b = added["object"]["box"].as_array().unwrap();
assert_eq!(
(b[0].as_f64().unwrap(), b[3].as_f64().unwrap()),
(16.0, 24.0)
);
let (err, upd) = call(
&mut ws,
6,
"update_object",
json!({"id": id, "rot": 90, "props": {"roof": "gable"}}),
);
assert!(!err, "{upd}");
assert_eq!(upd["object"]["rot"], 90.0);
let (err, bad) = call(
&mut ws,
7,
"update_object",
json!({"id": id, "props": {"floors": 99}}),
);
assert!(err && bad.as_str().unwrap().contains("outside"), "{bad}");
let (err, ops) = call(
&mut ws,
8,
"apply_ops",
json!({"ops": [
{"op": "add", "type": "tree", "x": 0, "y": 18},
{"op": "add", "type": "tree", "x": 4, "y": 18},
{"op": "remove", "id": "tree-1"}
]}),
);
assert!(!err, "{ops}");
let (err, view) = call(
&mut ws,
9,
"render_view",
json!({"level": "gf", "rotation": 2}),
);
assert!(!err && view["boxes"].get(&id).is_some(), "{view}");
let (err, svg) = call(&mut ws, 10, "render_view", json!({"format": "svg"}));
assert!(!err && svg.as_str().unwrap().starts_with("<svg"));
let (err, chk) = call(&mut ws, 11, "check_project", json!({}));
assert!(!err && chk["ok"] == true, "{chk}");
let (err, built) = call(&mut ws, 12, "build_html", json!({}));
assert!(!err && Path::new(built["path"].as_str().unwrap()).exists());
let (err, _) = call(&mut ws, 13, "undo", json!({}));
assert!(!err);
assert!(
ws.project.doc.get("tree-1").is_some(),
"undo brings back the batch"
);
let unknown = mcp::handle(
&mut ws,
&json!({"jsonrpc": "2.0", "id": 14, "method": "nope"}),
)
.unwrap();
assert_eq!(unknown["error"]["code"], -32601);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn scaffolded_project_works() {
let dir = std::env::temp_dir().join(format!("isokit-test-new-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
isokit_project::scaffold(&dir, "New site").unwrap();
let p = Project::load(&dir).unwrap();
assert_eq!(p.meta().title, "New site");
let f = check::check(&p);
assert!(f.is_empty(), "{f:?}");
assert!(isokit_project::scaffold(&dir, "again").is_err());
let _ = std::fs::remove_dir_all(&dir);
}