use concinnity_cook::authoring::registry::RegisteredType;
use crate::editor::hook::FormTarget;
use crate::editor::panels::asset_tree;
use crate::editor::panels::asset_tree::TreeGroup;
use crate::editor::panels::assets_panel::PanelAction;
use crate::editor::panels::form;
use crate::editor::widget;
use concinnity_core::ecs::World;
use crate::editor::hook::tests::fixtures::entry_target;
use crate::editor::hook::tests::fixtures::selected;
use crate::editor::hook::tests::fixtures::{
a_promotable_asset, click_row, entry, expandable_hook, hook, row_of, seed_tree,
world_with_fields,
};
use crate::editor::hook::{EditorHook, declared_id};
use crate::editor::overrides;
use crate::editor::panels::form_panel::{self, FormAction};
use crate::editor::panels::registry::PanelKey;
use crate::test_support::isolate_state_dir;
fn prefab_hook(patch: serde_json::Value) -> EditorHook {
isolate_state_dir();
let mut h = hook(vec![
serde_json::json!({"type":"ProceduralMesh","args":{"$id":"box","generator":"box"}}),
serde_json::json!({"type":"Prefab","args":{"$id":"pair","props":[
{"name":"a","kind":"prop","mesh":"box","position":[1.0,0.0,0.0]}]}}),
serde_json::json!({"type":"Prop","args":{"$id":"i1","prefab":"pair","position":[10.0,0.0,0.0]}}),
serde_json::json!({"type":"Prop","args": concinnity_cook::authoring::world::args_with_id(patch, "i1_a")}),
]);
h.open[PanelKey::Assets] = true;
h
}
fn entity_option(h: &EditorHook, label_prefix: &str) -> usize {
let labels: Vec<String> = h
.entity_menu_options()
.into_iter()
.map(|(_, l)| l)
.collect();
labels
.iter()
.position(|l| l.starts_with(label_prefix))
.unwrap_or_else(|| panic!("no entity option starting {label_prefix:?}, got {labels:?}"))
}
fn entry_of<'a>(h: &'a EditorHook, name: &str) -> Option<&'a serde_json::Value> {
h.entries.iter().find(|e| declared_id(e) == Some(name))
}
#[test]
fn apply_all_writes_the_mappable_paths_and_keeps_the_rest() {
let _guard = crate::test_support::lock();
let mut h = prefab_hook(serde_json::json!({
"position": [5.0, 0.0, 0.0],
"cull_distance": 42.0
}));
let mut world = world_with_fields();
h.open_asset_form("i1_a", &mut world);
let k = entity_option(&h, "Apply all to Prefab");
h.apply_form(FormAction::OpenEntityMenu, &mut world);
h.apply_form(FormAction::PickEntityOption(k), &mut world);
let def = entry_of(&h, "pair").expect("the definition stands");
assert_eq!(
def["args"]["props"][0]["position"],
serde_json::json!([-5.0, 0.0, 0.0]),
"the mappable path landed in the definition"
);
let patch = entry_of(&h, "i1_a").expect("the patch line survives its unmappable field");
assert!(
patch["args"].get("position").is_none(),
"the applied path left the patch"
);
assert_eq!(patch["args"]["cull_distance"], 42.0);
assert!(
h.form
.error
.as_ref()
.is_some_and(|e| e.contains("cull_distance")),
"the kept path is reported, got {:?}",
h.form.error
);
}
#[test]
fn apply_all_over_a_fully_mappable_patch_removes_the_line() {
let _guard = crate::test_support::lock();
let mut h = prefab_hook(serde_json::json!({"position": [5.0, 0.0, 0.0]}));
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let k = entity_option(&h, "Apply all to Prefab");
h.apply_form(FormAction::OpenEntityMenu, &mut world);
h.apply_form(FormAction::PickEntityOption(k), &mut world);
assert_eq!(
h.entries.len(),
before - 1,
"the emptied patch line is gone"
);
assert!(h.form.error.is_none(), "nothing was kept back");
h.undo(&mut world);
assert_eq!(h.entries.len(), before);
assert_eq!(
entry_of(&h, "pair").unwrap()["args"]["props"][0]["position"],
serde_json::json!([1.0, 0.0, 0.0]),
"one undo restores the definition and the patch line together"
);
}
fn baseline_of(h: &mut EditorHook, name: &str, key: &str) -> serde_json::Value {
let mut world = world_with_fields();
h.open_asset_form(name, &mut world);
h.form
.template
.as_ref()
.expect("a template-derived form")
.baseline
.get(key)
.cloned()
.unwrap_or(serde_json::Value::Null)
}
#[test]
fn minimize_removes_a_patch_that_matches_the_template() {
let _guard = crate::test_support::lock();
let mut probe = prefab_hook(serde_json::json!({"position": [5.0, 0.0, 0.0]}));
let inherited = baseline_of(&mut probe, "i1_a", "position");
let mut h = prefab_hook(serde_json::json!({ "position": inherited }));
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let k = entity_option(&h, "Minimize override");
h.apply_form(FormAction::OpenEntityMenu, &mut world);
h.apply_form(FormAction::PickEntityOption(k), &mut world);
assert_eq!(h.entries.len(), before - 1, "a no-op patch is not a patch");
assert!(entry_of(&h, "i1_a").is_none());
}
#[test]
fn minimize_keeps_only_the_fields_that_differ() {
let _guard = crate::test_support::lock();
let mut probe = prefab_hook(serde_json::json!({"position": [5.0, 0.0, 0.0]}));
let inherited = baseline_of(&mut probe, "i1_a", "position");
let mut h = prefab_hook(serde_json::json!({
"position": inherited,
"cull_distance": 42.0
}));
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let k = entity_option(&h, "Minimize override");
h.apply_form(FormAction::OpenEntityMenu, &mut world);
h.apply_form(FormAction::PickEntityOption(k), &mut world);
assert_eq!(
h.entries.len(),
before,
"the line stays for the real override"
);
let args = &entry_of(&h, "i1_a").expect("the patch line").clone()["args"];
assert_eq!(args["cull_distance"], 42.0);
assert!(
args.get("position").is_none(),
"the inherited field was stripped, got {args}"
);
}
#[test]
fn materializing_a_preset_prefab_authors_it_as_a_world_line() {
let _guard = crate::test_support::lock();
isolate_state_dir();
let dir = crate::project::assets_dir()
.expect("the harness opened a project")
.join("prefabs");
std::fs::create_dir_all(&dir).expect("preset dir");
std::fs::write(
dir.join("cn_test_prefab.json"),
r#"{"args":{"props":[{"name":"a","kind":"prop","mesh":"box","position":[2.0,0.0,0.0]}]}}"#,
)
.expect("preset file");
let mut h = hook(vec![
serde_json::json!({"type":"ProceduralMesh","args":{"$id":"box","generator":"box"}}),
serde_json::json!({"type":"Prop","args":{"$id":"i1","prefab":"cn_test_prefab","position":[10.0,0.0,0.0]}}),
serde_json::json!({"type":"Prop","args":{"$id":"i1_a","position":[5.0,0.0,0.0]}}),
]);
h.open[PanelKey::Assets] = true;
let mut world = world_with_fields();
h.open_asset_form("i1_a", &mut world);
let k = entity_option(&h, "Materialize Prefab");
h.apply_form(FormAction::OpenEntityMenu, &mut world);
h.apply_form(FormAction::PickEntityOption(k), &mut world);
let def = entry_of(&h, "cn_test_prefab").expect("the definition was authored");
assert_eq!(def["type"], "Prefab");
assert_eq!(
def["args"]["props"][0]["position"],
serde_json::json!([2.0, 0.0, 0.0]),
"the preset's entries came across verbatim"
);
let _ = std::fs::remove_file(dir.join("cn_test_prefab.json"));
}
#[test]
fn jump_to_override_cycles_through_the_marked_fields() {
let _guard = crate::test_support::lock();
let mut h = prefab_hook(serde_json::json!({
"position": [5.0, 0.0, 0.0],
"scale": [2.0, 2.0, 2.0]
}));
let mut world = world_with_fields();
h.open_asset_form("i1_a", &mut world);
let marked: Vec<usize> = h
.form_override_marks()
.expect("marks for a template form")
.iter()
.enumerate()
.filter(|(_, m)| **m != overrides::FieldOrigin::Inherited)
.map(|(i, _)| i)
.collect();
assert!(
marked.len() >= 2,
"both pinned fields are marked, got {marked:?}"
);
let max = h.form.fields.len().saturating_sub(h.form_window());
let mut seen = Vec::new();
for _ in 0..marked.len() + 1 {
h.jump_to_override(&mut world);
seen.push(h.form.scroll);
}
assert!(
seen.iter().all(|s| marked.contains(s) || *s == max),
"every stop is a marked field (or the scroll clamp), got {seen:?} for {marked:?}"
);
assert!(
seen.first() == seen.last() || seen.len() > marked.len(),
"the walk wraps rather than sticking, got {seen:?}"
);
}
#[test]
fn jump_to_override_is_a_no_op_without_marks() {
let _guard = crate::test_support::lock();
let mut h = prefab_hook(serde_json::json!({}));
let mut world = world_with_fields();
h.open_asset_form("i1_a", &mut world);
h.form.scroll = 0;
h.jump_to_override(&mut world);
assert_eq!(h.form.scroll, 0);
}
fn diverge_a_form_field(h: &mut EditorHook, world: &mut World) -> String {
if let Some(j) = h
.form
.fields
.iter()
.position(|f| matches!(f.kind, form::FieldKind::Bool))
{
let key = h.form.fields[j].key.clone();
h.apply_form(FormAction::ToggleField(j), world);
return key;
}
let window = h.form_window();
let j = h
.form
.fields
.iter()
.position(|f| {
matches!(
f.kind,
form::FieldKind::Str | form::FieldKind::Int | form::FieldKind::Float
)
})
.filter(|&j| j < window)
.expect("a visible scalar field to diverge");
let key = h.form.fields[j].key.clone();
widget::seed_field(world, form_panel::form_input(j), "42424");
key
}
#[test]
fn editing_a_generated_asset_writes_a_minimal_patch_on_confirm() {
let _guard = crate::test_support::lock();
let mut h = expandable_hook();
let mut world = world_with_fields();
h.refresh_tree_if_needed();
let (gi, ai, name) = a_promotable_asset(&h);
let before = h.entries.len();
h.apply_panel(PanelAction::SelectRow(gi, ai), &mut world);
assert!(h.form_open(), "a generated asset still opens the form");
assert!(
matches!(h.form.target, FormTarget::Promote(_)),
"seeded from the expansion, not from an entry"
);
assert!(
h.form.template.is_some(),
"the form knows its template baseline"
);
assert_eq!(
widget::field_text(&world, form_panel::NAME_INPUT),
name,
"the name heading carries the generated name"
);
assert!(selected(&h).contains(&name), "and the row selects");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(
h.entries.len(),
before,
"an unchanged confirm authors nothing"
);
h.apply_panel(PanelAction::SelectRow(gi, ai), &mut world);
let key = diverge_a_form_field(&mut h, &mut world);
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), before + 1, "a divergence appends the line");
let added = h.entries.last().unwrap();
assert_eq!(
declared_id(added),
Some(name.as_str()),
"the patch line keeps the generated name, so it patches it"
);
let root = key.split('.').next().unwrap().to_string();
let args = added.get("args").and_then(|a| a.as_object()).unwrap();
assert_eq!(
args.keys().filter(|k| *k != "$id").collect::<Vec<_>>(),
vec![&root],
"only the diverged field is authored"
);
assert!(h.dirty && h.assets.stale);
}
#[test]
fn a_patched_asset_relists_under_its_origin_as_overridden() {
let _guard = crate::test_support::lock();
let mut h = expandable_hook();
let mut world = world_with_fields();
h.refresh_tree_if_needed();
let (gi, ai, name) = a_promotable_asset(&h);
let origin = h.assets.groups[gi].label.clone();
h.apply_panel(PanelAction::SelectRow(gi, ai), &mut world);
diverge_a_form_field(&mut h, &mut world);
h.apply_form(FormAction::Confirm, &mut world);
h.refresh_tree_if_needed();
let listings: Vec<&str> = h
.assets
.groups
.iter()
.filter(|g| g.assets.iter().any(|a| a.name == name))
.map(|g| g.label.as_str())
.collect();
assert_eq!(
listings,
[origin.as_str()],
"listed once, under its origin group"
);
let patched = h
.assets
.groups
.iter()
.flat_map(|g| &g.assets)
.find(|a| a.name == name)
.unwrap();
assert_eq!(patched.badge, asset_tree::Badge::Overridden);
let (g2, i2) = row_of(&h, &name);
let before = h.entries.len();
h.apply_panel(PanelAction::SelectRow(g2, i2), &mut world);
assert!(matches!(h.form.target, FormTarget::Entry(_)));
assert!(h.form.template.is_some());
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), before, "edited in place, not appended");
}
fn two_instance_prefab_hook() -> EditorHook {
isolate_state_dir();
let mut h = hook(vec![
serde_json::json!({"type":"ProceduralMesh","args":{"$id":"box","generator":"box"}}),
serde_json::json!({"type":"Prefab","args":{"$id":"pair","props":[
{"name":"a","kind":"prop","mesh":"box","position":[1.0,0.0,0.0]}]}}),
serde_json::json!({"type":"Prop","args":{"$id":"i1","prefab":"pair","position":[10.0,0.0,0.0]}}),
serde_json::json!({"type":"Prop","args":{"$id":"i2","prefab":"pair"}}),
serde_json::json!({"type":"Prop","args":{"$id":"i1_a","position":[5.0,0.0,0.0]}}),
]);
h.open[PanelKey::Assets] = true;
h
}
#[test]
fn an_overridden_field_marks_and_offers_revert_and_apply() {
let _guard = crate::test_support::lock();
let mut h = two_instance_prefab_hook();
let mut world = world_with_fields();
h.open_asset_form("i1_a", &mut world);
assert!(h.form.template.is_some(), "i1_a derives from the prefab");
let marks = h.form_override_marks().expect("marks for a template form");
let j = h
.form
.fields
.iter()
.position(|f| f.key == "position")
.expect("a position field");
assert_eq!(marks[j], overrides::FieldOrigin::Overridden);
assert!(
marks
.iter()
.enumerate()
.filter(|(i, _)| !h.form.fields[*i].key.starts_with("position"))
.all(|(_, m)| *m == overrides::FieldOrigin::Inherited),
"only the pinned field is marked"
);
let options = h.override_menu_options(j);
let labels: Vec<&str> = options.iter().map(|(_, l)| l.as_str()).collect();
assert_eq!(labels[0], "Revert 'position'");
assert_eq!(labels[1], "Apply to Prefab 'pair' (updates 2 instances)");
}
#[test]
fn reverting_the_only_override_removes_the_patch_line_and_undoes_in_one_step() {
let _guard = crate::test_support::lock();
let mut h = two_instance_prefab_hook();
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let j = h
.form
.fields
.iter()
.position(|f| f.key == "position")
.unwrap();
h.apply_form(FormAction::OpenOverrideMenu(j), &mut world);
h.apply_form(FormAction::PickOverrideOption(0), &mut world);
assert_eq!(h.entries.len(), before - 1, "the patch line is gone");
assert!(
!h.entries.iter().any(|e| declared_id(e) == Some("i1_a")),
"the asset is pristine again"
);
assert!(h.form_open(), "the form re-derives instead of closing");
assert!(
h.form_override_marks()
.unwrap()
.iter()
.all(|m| *m == overrides::FieldOrigin::Inherited)
);
h.undo(&mut world);
assert_eq!(h.entries.len(), before, "one undo restores the patch line");
assert!(h.entries.iter().any(|e| declared_id(e) == Some("i1_a")));
}
#[test]
fn applying_an_override_updates_the_prefab_entry_and_drops_the_patch() {
let _guard = crate::test_support::lock();
let mut h = two_instance_prefab_hook();
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let j = h
.form
.fields
.iter()
.position(|f| f.key == "position")
.unwrap();
h.apply_form(FormAction::OpenOverrideMenu(j), &mut world);
h.apply_form(FormAction::PickOverrideOption(1), &mut world);
let def = h
.entries
.iter()
.find(|e| declared_id(e) == Some("pair"))
.unwrap();
assert_eq!(
def["args"]["props"][0]["position"],
serde_json::json!([-5.0, 0.0, 0.0])
);
assert_eq!(
h.entries.len(),
before - 1,
"the emptied patch line is gone; the template now carries the value"
);
h.undo(&mut world);
let def = h
.entries
.iter()
.find(|e| declared_id(e) == Some("pair"))
.unwrap();
assert_eq!(
def["args"]["props"][0]["position"],
serde_json::json!([1.0, 0.0, 0.0]),
"one undo restores the definition and the patch line together"
);
assert_eq!(h.entries.len(), before);
}
#[test]
fn the_entity_menu_reverts_all_overrides() {
let _guard = crate::test_support::lock();
let mut h = two_instance_prefab_hook();
let mut world = world_with_fields();
let before = h.entries.len();
h.open_asset_form("i1_a", &mut world);
let labels: Vec<String> = h
.entity_menu_options()
.into_iter()
.map(|(_, l)| l)
.collect();
assert!(
labels.iter().any(|l| l == "Revert all overrides"),
"{labels:?}"
);
assert!(
labels
.iter()
.any(|l| l == "Apply all to Prefab 'pair' (updates 2 instances)"),
"{labels:?}"
);
h.apply_form(FormAction::OpenEntityMenu, &mut world);
let k = labels
.iter()
.position(|l| l == "Revert all overrides")
.unwrap();
h.apply_form(FormAction::PickEntityOption(k), &mut world);
assert_eq!(h.entries.len(), before - 1);
assert!(!h.entries.iter().any(|e| declared_id(e) == Some("i1_a")));
h.undo(&mut world);
assert_eq!(h.entries.len(), before, "revert-all is one undo step");
}
#[test]
fn an_unconditional_expansion_selects_but_does_not_edit() {
let mut h = hook(vec![entry("lamp", "PointLight")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(
&mut h,
vec![TreeGroup {
label: asset_tree::UNATTRIBUTED.to_string(),
assets: vec![asset_tree::TreeAsset {
name: "menu_tab_0".to_string(),
asset_type: RegisteredType::TextLabel,
badge: asset_tree::Badge::Imported,
promote: None,
}],
}],
);
click_row(&mut h, "lamp", &mut world);
assert!(h.form_open(), "the authored line opens its form");
click_row(&mut h, "menu_tab_0", &mut world);
assert!(!h.form_open(), "a fixed expansion has nothing to edit");
assert!(
selected(&h).iter().any(|n| n == "menu_tab_0"),
"but it still selects in the viewport"
);
assert!(h.entries.len() == 1, "and nothing was appended");
}
#[test]
fn unapplied_markers_follow_edit_and_apply() {
let mut h = hook(vec![entry("cube", "PointLight")]);
let mut world = world_with_fields();
let target = entry_target(&h, 0);
h.open_form(&mut world, "PointLight".to_string(), target);
assert!(!h.form.touched);
assert!(!h.panel_data(&world).form_title.ends_with('*'));
h.apply_form(FormAction::CycleField(0), &mut world);
assert!(h.form.touched, "a control edit marks the form");
assert!(h.panel_data(&world).form_title.ends_with('*'));
let target = entry_target(&h, 0);
h.open_form(&mut world, "PointLight".to_string(), target);
h.apply_form(FormAction::FocusName, &mut world);
assert!(!h.form.touched, "focus is not an edit");
h.form.touched = true;
h.form.close();
assert!(!h.form.touched, "closing discards the marker");
h.lighting.touched = true;
h.seed_lighting(&mut world);
assert!(!h.lighting.touched);
h.story.area = crate::editor::text_area::TextArea::from_text("hello");
h.story.area.type_char('!');
assert!(h.make_story_view([0.0, 0.0]).area.is_dirty());
h.story.area.undo();
assert!(!h.make_story_view([0.0, 0.0]).area.is_dirty());
}