//! Runtime tests: the scripting API, prefabs, the document driver, errors. use crate::doc::{Doc, Object}; use crate::{Bundle, Runtime}; use isokit_core::{render, RenderOpts}; use serde_json::{json, Map, Value}; fn rt(main: &str) -> Runtime { let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() }; b.sources.insert("scene.rhai".into(), main.into()); Runtime::new(&b).unwrap_or_else(|e| panic!("{e}")) } fn cfg() -> Map { Map::new() } fn obj(id: &str, kind: &str, x: f64, y: f64, props: Value) -> Object { Object { id: id.into(), kind: kind.into(), x, y, props: props.as_object().cloned().unwrap_or_default(), ..Default::default() } } #[test] fn script_draws_with_mixed_numbers() { let r = rt(r#" let s = sprite("hall"); s.box(0, 0, 10, 6.5, 0, 3, "wall", #{ top: "roof" }); info("hall", "Hall", "10 x 6.5 m"); tag(5, 3, 4, "Hall", 5); "#); let b = r.build("roof", 0, &cfg(), &Doc::default()).unwrap(); let out = render(&b.scene, 0, &RenderOpts::default()); assert!(out.html.contains("iso-roof iso-t"), "{}", out.html); assert!(out.html.contains("iso-wall iso-l")); assert!(out.html.contains(">Hall<")); assert_eq!(out.info["hall"].detail, "10 x 6.5 m"); } #[test] fn methods_from_the_library_work_on_sprites() { let r = rt(r#" let s = sprite("t"); s.tree_at(0, 0, 1.5); sprite("c").car(0, 0, 4.4, 1.8, "car2"); "#); let b = r.build("site", 1, &cfg(), &Doc::default()).unwrap(); let out = render(&b.scene, 1, &RenderOpts::default()); assert!(out.html.contains("iso-leaf iso-l")); assert!(out.html.contains("iso-glassd")); } #[test] fn seq_group_and_closures() { let r = rt(r#" let s = sprite("x"); let h = 2.0; s.seq([[0, 0, |q| q.box(0, 0, 1, 1, 0, h, "wall")], [5, 0, |q| q.box(5, 0, 6, 1, 0, h, "roof")]]); s.group("a drive", |q| q.box(0, 3, 1, 4, 0, 1, "car1")); "#); let b = r.build("site", 0, &cfg(), &Doc::default()).unwrap(); let out = render(&b.scene, 0, &RenderOpts::default()); // from the south-east the box at x = 5 is nearer, so it is drawn after the one at x = 0 let (a, c) = ( out.html.find("iso-wall").unwrap(), out.html.find("iso-roof").unwrap(), ); assert!(a < c); assert!(out.html.contains("")); } #[test] fn errors_point_at_the_line() { let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() }; b.sources.insert( "scene.rhai".into(), "let s = sprite(\"a\");\n\ns.box(0, 0, 1, 1, 0, \"high\", \"wall\");\n".into(), ); let r = Runtime::new(&b).unwrap(); let e = r .build("site", 0, &cfg(), &Doc::default()) .err() .expect("error"); assert_eq!(e.file, "scene.rhai"); assert_eq!(e.line, Some(3), "{e}"); assert!(e.message.contains("number"), "{e}"); b.sources.insert("scene.rhai".into(), "let x = ;".into()); let e = Runtime::new(&b).err().expect("parse error"); assert_eq!(e.line, Some(1), "{e}"); } #[test] fn imports_resolve_from_the_bundle() { let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() }; b.sources.insert( "lib/kiosk.rhai".into(), "fn kiosk(x, y) { sprite(\"kiosk\").box(x, y, x + 2, y + 2, 0, 2.5, \"wall\"); }".into(), ); b.sources.insert( "scene.rhai".into(), "import \"lib/kiosk\" as k;\nk::kiosk(1, 2);".into(), ); let r = Runtime::new(&b).unwrap(); let s = r.build("site", 0, &cfg(), &Doc::default()).unwrap().scene; assert_eq!(s.sprite_count(), 1); } #[test] fn catalog_lists_library_and_project_prefabs() { let mut b = Bundle { main: None, prelude: vec!["lib/kiosk.rhai".into()], ..Default::default() }; b.sources.insert( "lib/kiosk.rhai".into(), r#" fn meta_kiosk() { #{ label: "Kiosk", category: "Shop", props: [ #{ key: "h", type: "float", def: 2.5 } ] } } fn prefab_kiosk(o) { sprite().box(-1, -1, 1, 1, 0, o.p.h, "wall"); } "# .into(), ); let r = Runtime::new(&b).unwrap(); let kinds: Vec<&str> = r.catalog().iter().map(|m| m.kind.as_str()).collect(); for k in [ "site", "area", "road", "building", "tree", "car", "person", "tank", "kiosk", ] { assert!(kinds.contains(&k), "{k} missing from {kinds:?}"); } let k = r.prefab("kiosk").unwrap(); assert_eq!(k.label, "Kiosk"); assert_eq!(k.source, "lib/kiosk.rhai"); assert_eq!(k.props[0].default, json!(2.5)); } #[test] fn document_objects_are_placed_in_their_frame() { let r = Runtime::new(&Bundle::default()).unwrap(); let mut doc = Doc::default(); let mut b = obj( "hall", "building", 30.0, 20.0, json!({"w": 10, "d": 6, "floors": 1}), ); b.rot = 90.0; b.name = "Main hall".into(); b.tag = "Hall".into(); doc.objects.push(b); doc.objects.push(obj("t1", "tree", 5.0, 5.0, json!({}))); let built = r.build("roof", 1, &cfg(), &doc).unwrap(); // turned a quarter: 6 m along x, 10 m along y, centred on (30, 20) let bx = *built .boxes .get("hall") .unwrap_or_else(|| panic!("{:?}", built.scene.warnings)); assert!( (bx[0] - 27.0).abs() < 1e-9 && (bx[3] - 33.0).abs() < 1e-9, "{bx:?}" ); assert!( (bx[1] - 15.0).abs() < 1e-9 && (bx[4] - 25.0).abs() < 1e-9, "{bx:?}" ); let out = render(&built.scene, 1, &RenderOpts::default()); assert!(out.html.contains("data-obj=\"hall\"")); assert!(out.html.contains("data-obj=\"t1\"")); assert_eq!(out.info["hall"].name, "Main hall"); assert_eq!(out.info["t1"].name, "Tree"); assert!(out.html.contains(">Hall<")); assert!( built.scene.warnings.is_empty(), "{:?}", built.scene.warnings ); } #[test] fn levels_cut_buildings_open_and_filter_objects() { let r = Runtime::new(&Bundle::default()).unwrap(); let mut doc = Doc::default(); doc.objects .push(obj("b", "building", 0.0, 0.0, json!({"floors": 2}))); let mut desk = obj("desk", "tree", 0.0, 0.0, json!({})); desk.levels = vec!["gf".into()]; doc.objects.push(desk); let closed = r.build("roof", 0, &cfg(), &doc).unwrap(); let gf = r.build("gf", 0, &cfg(), &doc).unwrap(); let up = r.build("3f", 0, &cfg(), &doc).unwrap(); assert!(!closed.boxes.contains_key("desk")); assert!(gf.boxes.contains_key("desk")); // cut open: a floor sprite plus four wall sprites assert!(gf.scene.sprite_count() > closed.scene.sprite_count()); let html = render(&gf.scene, 0, &RenderOpts::default()).html; assert!(html.contains("iso-z-floor iso-t")); let html = render(&up.scene, 0, &RenderOpts::default()).html; assert!( html.contains("class=\"iso-dim\""), "ghosted above the top storey" ); } #[test] fn every_library_prefab_renders_cleanly_in_every_rotation() { let r = Runtime::new(&Bundle::default()).unwrap(); let mut doc = Doc::default(); for (i, m) in r.catalog().iter().enumerate() { let mut o = obj(&format!("o{i}"), &m.kind, i as f64 * 15.0, 0.0, json!({})); // switch every bool prop on to exercise all branches for p in &m.props { if p.kind == "bool" { o.props.insert(p.key.clone(), json!(true)); } } doc.objects.push(o); } for level in ["site", "gf", "1f"] { for rot in 0..4 { let b = r .build(level, rot, &cfg(), &doc) .unwrap_or_else(|e| panic!("{level}/{rot}: {e}")); assert!( b.scene.warnings.is_empty(), "{level}/{rot}: {:?}", b.scene.warnings ); let out = render(&b.scene, rot, &RenderOpts::default()); assert!(!out.html.contains("NaN"), "{level}/{rot}"); for m in r.catalog() { let id = format!( "o{}", r.catalog().iter().position(|q| q.kind == m.kind).unwrap() ); assert!( b.boxes.contains_key(&id), "{} drew nothing in {level}/{rot}", m.kind ); } } } } #[test] fn unknown_types_and_failing_prefabs_warn_but_render() { let mut b = Bundle { main: None, prelude: vec!["lib/bad.rhai".into()], ..Default::default() }; b.sources.insert( "lib/bad.rhai".into(), "fn prefab_bad(o) { sprite().box(0, 0, 1, 1, 0, o.p.nope, \"wall\"); }".into(), ); let r = Runtime::new(&b).unwrap(); let mut doc = Doc::default(); doc.objects.push(obj("u", "ufo", 0.0, 0.0, json!({}))); doc.objects.push(obj("x", "bad", 0.0, 0.0, json!({}))); doc.objects.push(obj("t", "tree", 5.0, 0.0, json!({}))); let built = r.build("site", 0, &cfg(), &doc).unwrap(); let w = built.scene.warnings.join("\n"); assert!(w.contains("object u: unknown type"), "{w}"); assert!(w.contains("object x:"), "{w}"); assert!(built.boxes.contains_key("t")); } #[test] fn cfg_reaches_the_script() { let r = rt( r#"if cfg("big") { sprite("a").box(0, 0, 9, 9, 0, 9, "wall"); } else { sprite("a").box(0, 0, 1, 1, 0, 1, "wall"); }"#, ); let mut c = Map::new(); c.insert("big".into(), json!(true)); let b = r.build("site", 0, &c, &Doc::default()).unwrap(); assert_eq!(b.scene.layers[2].sprites[0].world_box().unwrap()[5], 9.0); }