use concinnity_core::components::ShaderStage;
use concinnity_core::gfx::render_types::MAX_SHADER_BUCKETS;
use std::path::Path;
use super::shader_fixtures::{
args, blocked, click, confirm, detail, form_world, in_project, key, material, plain_material,
press, rows, shader, shader_names, toasts, type_name, write,
};
use crate::editor::hook::FormTarget;
use crate::editor::hook::tests::fixtures::{hook, press_modal};
use crate::editor::panels::form_panel;
use crate::editor::panels::registry::PanelKey;
use crate::editor::panels::shader_list::{self, RowKind};
use crate::editor::panels::shader_templates;
#[test]
fn a_new_shader_is_created_through_its_form() {
in_project(|root| {
let shaders = root.join("assets/shaders");
std::fs::create_dir_all(&shaders).unwrap();
std::fs::write(shaders.join("water.hlsl"), "someone else's").unwrap();
let mut h = hook(vec![plain_material("sea"), plain_material("rock")]);
let mut world = form_world();
click(&mut h, &mut world, RowKind::New);
assert_eq!(h.form.selected_type.as_deref(), Some("Shader"));
assert_eq!(h.form.target, FormTarget::New);
assert_eq!(h.form.host, PanelKey::Shaders);
assert!(
h.form.fields.is_empty(),
"the file fields are the form's own"
);
type_name(&mut world, " ");
let reason = blocked(&h, &world).unwrap();
assert!(reason.contains("Enter a name"), "{reason}");
confirm(&mut h, &mut world);
assert!(h.entries.iter().all(|e| e["type"] != "Shader"));
assert!(h.form_open(), "the form stays open with the reason");
type_name(&mut world, " water ");
assert_eq!(blocked(&h, &world), None);
assert_eq!(
detail(&h, &world, "fragment").as_deref(),
Some("assets/shaders/water_1.hlsl"),
"the preview numbers past a file already there"
);
press(&mut h, &mut world, "vertex", false);
press(&mut h, &mut world, "fragment", true);
press(&mut h, &mut world, "vertex", true);
press(&mut h, &mut world, "sea", false);
confirm(&mut h, &mut world);
assert!(!h.form_open());
assert_eq!(shader_names(&h), ["water"]);
let water = args(&h, "water");
assert_eq!(water["fragment"], "assets/shaders/water_1.hlsl");
assert_eq!(water["vertex"], "assets/shaders/water_vertex.hlsl");
let read = |p: &str| std::fs::read_to_string(root.join(p)).unwrap();
assert_eq!(
read("assets/shaders/water_1.hlsl"),
shader_templates::FRAGMENT[1].text
);
assert_eq!(
read("assets/shaders/water_vertex.hlsl"),
shader_templates::VERTEX[1].text
);
assert_eq!(read("assets/shaders/water.hlsl"), "someone else's");
assert_eq!(args(&h, "sea")["shader"], "water");
assert!(args(&h, "rock").get("shader").is_none());
let src = h.shaders.source.as_ref().unwrap();
assert_eq!(src.key, key("water", ShaderStage::Fragment));
assert!(
toasts(&h)
.iter()
.any(|t| t.contains("Added Shader 'water'"))
);
h.undo(&mut world);
assert!(shader_names(&h).is_empty());
assert!(args(&h, "sea").get("shader").is_none());
assert!(!h.can_undo(), "one step");
assert!(
root.join("assets/shaders/water_1.hlsl").exists(),
"files stay"
);
});
}
#[test]
fn a_taken_name_or_the_limit_blocks_the_form() {
let mut h = hook(vec![
shader("lit", Path::new("/cn-none/lit.hlsl")),
plain_material("rock"),
]);
let mut world = form_world();
h.open_form(&mut world, "Shader".to_string(), FormTarget::New);
for (name, why) in [
("lit", "already taken"),
("rock", "already taken"),
("Shader#3", "reserved"),
] {
type_name(&mut world, name);
let reason = blocked(&h, &world).unwrap();
assert!(reason.contains(why), "{name}: {reason}");
}
confirm(&mut h, &mut world);
assert_eq!(shader_names(&h), ["lit"]);
assert!(h.form.error.as_deref().unwrap().contains("reserved"));
let full: Vec<serde_json::Value> = (0..MAX_SHADER_BUCKETS)
.map(|i| shader(&format!("s{i}"), Path::new("/cn-none/s.hlsl")))
.collect();
let mut h = hook(full);
h.open_form(&mut world, "Shader".to_string(), FormTarget::New);
type_name(&mut world, "more");
assert!(blocked(&h, &world).unwrap().contains("No room"));
confirm(&mut h, &mut world);
assert_eq!(shader_list::shader_count(&h.entries), MAX_SHADER_BUCKETS);
assert!(!h.dirty);
}
#[test]
fn a_rename_through_the_form_rewrites_every_reference() {
let dir = tempfile::tempdir().unwrap();
let lit = write(dir.path(), "lit.hlsl");
let water = write(dir.path(), "water.hlsl");
let mut h = hook(vec![
shader("lit", &lit),
shader("water", &water),
material("sea", "water"),
material("pond", "water"),
material("stone", "lit"),
]);
let mut world = form_world();
h.open_shader_file(key("water", ShaderStage::Fragment));
click(&mut h, &mut world, RowKind::Header(1));
assert!(h.form.target.is_edit());
assert_eq!(
crate::editor::widget::field_text(&world, form_panel::NAME_INPUT),
"water",
"seeded with the current name"
);
type_name(&mut world, "lit");
assert!(blocked(&h, &world).unwrap().contains("already taken"));
type_name(&mut world, "ocean");
confirm(&mut h, &mut world);
assert_eq!(shader_names(&h), ["lit", "ocean"]);
assert_eq!(args(&h, "sea")["shader"], "ocean");
assert_eq!(args(&h, "pond")["shader"], "ocean");
assert_eq!(args(&h, "stone")["shader"], "lit");
assert_eq!(
args(&h, "ocean")["fragment"],
water.to_string_lossy().as_ref()
);
assert_eq!(
h.shaders.source.as_ref().unwrap().key,
key("ocean", ShaderStage::Fragment)
);
assert!(
toasts(&h)
.iter()
.any(|t| t == "Renamed Shader 'water' to 'ocean' and the 2 references to it")
);
h.undo(&mut world);
assert_eq!(shader_names(&h), ["lit", "water"]);
assert_eq!(args(&h, "sea")["shader"], "water");
assert_eq!(
h.shaders.source.as_ref().unwrap().key,
key("water", ShaderStage::Fragment)
);
assert!(!h.can_undo(), "the rename was one step");
}
#[test]
fn stages_add_and_drop_the_vertex_file() {
in_project(|root| {
let lit = write(root, "lit.hlsl");
let mut h = hook(vec![shader("lit", &lit)]);
let mut world = form_world();
click(&mut h, &mut world, RowKind::Header(0));
assert!(!rows(&h, &world).iter().any(|r| r.caption == "Starters"));
press(&mut h, &mut world, "vertex", false);
assert_eq!(
detail(&h, &world, "vertex").as_deref(),
Some("assets/shaders/lit_vertex.hlsl")
);
confirm(&mut h, &mut world);
let declared = args(&h, "lit")["vertex"].as_str().unwrap().to_string();
assert_eq!(declared, "assets/shaders/lit_vertex.hlsl");
let path = root.join(&declared);
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
shader_templates::VERTEX[0].text
);
let vertex = key("lit", ShaderStage::Vertex);
assert_eq!(h.shaders.source.as_ref().unwrap().key, vertex);
h.shaders.source.as_mut().unwrap().area.type_char('x');
click(&mut h, &mut world, RowKind::Header(0));
press(&mut h, &mut world, "vertex", false);
confirm(&mut h, &mut world);
assert!(h.modal.is_some(), "asked over the unsaved vertex file");
press_modal(&mut h, &mut world, "Cancel");
assert!(args(&h, "lit").get("vertex").is_some(), "Cancel keeps it");
assert!(h.form_open() && h.shaders.source.is_some());
confirm(&mut h, &mut world);
press_modal(&mut h, &mut world, "Discard");
assert!(args(&h, "lit").get("vertex").is_none());
assert!(h.shaders.source.is_none());
assert!(!h.form_open());
assert!(path.exists(), "the file stays");
h.undo(&mut world);
assert_eq!(args(&h, "lit")["vertex"], declared.as_str());
});
}
#[test]
fn used_by_assigns_and_unassigns_in_one_step() {
let mut h = hook(vec![
shader("lit", Path::new("/cn-none/lit.hlsl")),
shader("water", Path::new("/cn-none/water.hlsl")),
material("a", "water"),
material("b", "lit"),
plain_material("c"),
]);
let mut world = form_world();
click(&mut h, &mut world, RowKind::Header(1));
assert_eq!(detail(&h, &world, "b").as_deref(), Some("names 'lit'"));
for m in ["a", "b", "c"] {
press(&mut h, &mut world, m, false);
}
confirm(&mut h, &mut world);
assert!(args(&h, "a").get("shader").is_none());
assert_eq!(args(&h, "b")["shader"], "water");
assert_eq!(args(&h, "c")["shader"], "water");
h.undo(&mut world);
assert_eq!(args(&h, "a")["shader"], "water");
assert_eq!(args(&h, "b")["shader"], "lit");
assert!(args(&h, "c").get("shader").is_none());
assert!(!h.can_undo(), "one step");
}
#[test]
fn the_world_default_moves_through_the_form() {
let mut h = hook(vec![
shader("a", Path::new("/cn-none/a.hlsl")),
plain_material("m"),
shader("b", Path::new("/cn-none/b.hlsl")),
shader("c", Path::new("/cn-none/c.hlsl")),
]);
let mut world = form_world();
click(&mut h, &mut world, RowKind::Header(2));
press(&mut h, &mut world, "World default", false);
assert!(
rows(&h, &world)
.iter()
.any(|r| r.caption == "Materials naming no Shader switch to it")
);
confirm(&mut h, &mut world);
assert_eq!(shader_names(&h), ["c", "a", "b"]);
assert!(h.shader_rows()[0].badge.is_some(), "listed as the default");
click(&mut h, &mut world, RowKind::Header(0));
press(&mut h, &mut world, "World default", false);
assert!(
rows(&h, &world)
.iter()
.any(|r| r.caption == "'a' becomes the world default")
);
confirm(&mut h, &mut world);
assert_eq!(shader_names(&h), ["a", "c", "b"]);
h.undo(&mut world);
h.undo(&mut world);
assert_eq!(shader_names(&h), ["a", "b", "c"]);
}
#[test]
fn the_form_belongs_to_the_panel_that_opened_it() {
let mut h = hook(vec![shader("lit", Path::new("/cn-none/lit.hlsl"))]);
let mut world = form_world();
let edit = crate::editor::panels::registry::panel(PanelKey::Edit);
h.open[PanelKey::Shaders] = true;
click(&mut h, &mut world, RowKind::Header(0));
assert!(edit.is_open(&h), "shown beside the Shaders panel");
h.open[PanelKey::Shaders] = false;
assert!(!edit.is_open(&h));
h.open_form(&mut world, "PointLight".to_string(), FormTarget::New);
assert_eq!(h.form.host, PanelKey::Assets);
h.open[PanelKey::Assets] = true;
assert!(edit.is_open(&h));
}