use concinnity_core::components::FrameInput;
use concinnity_core::components::Sprite;
use concinnity_core::components::TextInput;
use concinnity_core::ecs::World;
use super::fixtures::{
click_row, entry, entry_with_args, hook, row_of, seed_tree, set_field, world_with_fields,
};
use crate::editor::hook::{EditorHook, FormTarget, declared_id, visible_slot};
use crate::frame_hook::FrameHook;
use crate::editor::inject;
use crate::editor::panels::form;
use crate::editor::panels::form_panel::{self, FormAction};
use crate::editor::panels::assets_panel::{self, PanelAction};
use crate::editor::panels::registry::PanelKey;
use crate::editor::hook::tests::fixtures::{active, entry_target, select, selected};
use crate::editor::widget;
#[test]
fn field_snapshot_carries_typed_text_across_a_reinjection() {
let mut old = World::new();
inject::editor_hud(&mut old);
widget::seed_field(&mut old, form_panel::NAME_INPUT, "my_light");
widget::seed_field(&mut old, assets_panel::SEARCH_INPUT, "Point");
let snapshot = EditorHook::field_snapshot(&old);
let mut new = World::new();
inject::editor_hud(&mut new);
assert_eq!(widget::field_text(&new, form_panel::NAME_INPUT), "");
EditorHook::restore_fields(&mut new, &snapshot);
assert_eq!(widget::field_text(&new, form_panel::NAME_INPUT), "my_light");
assert_eq!(
widget::field_text(&new, assets_panel::SEARCH_INPUT),
"Point"
);
}
#[test]
fn the_search_field_narrows_the_picker_while_it_is_open() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.apply_panel(PanelAction::TogglePicker, &mut world);
set_field(&mut world, assets_panel::SEARCH_INPUT, "pointlight");
let opts = h.picker_options(&world).unwrap();
let names: Vec<&str> = opts.iter().map(|o| o.name.as_str()).collect();
assert_eq!(names, ["PointLight"], "case-insensitive type narrowing");
assert!(
h.tree_rows(&world).is_empty(),
"the tree is not filtered by the picker's text"
);
}
#[test]
fn plus_picker_then_name_form_adds_the_entry() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.apply_panel(PanelAction::TogglePicker, &mut world);
assert!(h.assets.picker_open && h.assets.search_focus);
let ty = h.picker_options(&world).unwrap()[0].name.clone();
h.apply_panel(PanelAction::PickOption(0), &mut world);
assert!(h.form_open());
assert!(!h.assets.picker_open);
assert_eq!(h.form.selected_type.as_deref(), Some(ty.as_str()));
assert!((h.form.target == FormTarget::New));
let name_field = world
.get_by_id::<TextInput>(form_panel::NAME_INPUT)
.unwrap();
assert!(name_field.focused && !name_field.content.is_empty());
set_field(&mut world, form_panel::NAME_INPUT, "my_light");
h.apply_form(FormAction::Confirm, &mut world);
assert!(!h.form_open());
assert!(h.dirty);
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["args"]["$id"], "my_light");
assert_eq!(h.entries[0]["type"], ty.as_str());
}
#[test]
fn row_click_opens_the_edit_form_for_a_rename() {
let mut h = hook(vec![entry("lamp", "PointLight")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
click_row(&mut h, "lamp", &mut world);
assert!(h.form_open());
assert_eq!(h.form.target, entry_target(&h, 0));
assert_eq!(h.form.selected_type.as_deref(), Some("PointLight"));
assert!(h.assets.row_menu.is_none());
let name_field = world
.get_by_id::<TextInput>(form_panel::NAME_INPUT)
.unwrap();
assert_eq!(name_field.content, "lamp", "name prefilled from the entry");
set_field(&mut world, form_panel::NAME_INPUT, "streetlamp");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), 1, "edited in place, not appended");
assert_eq!(h.entries[0]["args"]["$id"], "streetlamp");
assert_eq!(h.entries[0]["type"], "PointLight");
assert!(h.dirty);
}
#[test]
fn renaming_an_asset_through_the_form_keeps_it_selected() {
let mut h = hook(vec![entry("floor", "Decal"), entry("lamp", "PointLight")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
click_row(&mut h, "lamp", &mut world);
assert_eq!(active(&h).as_deref(), Some("lamp"));
set_field(&mut world, form_panel::NAME_INPUT, "streetlamp");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries[1]["args"]["$id"], "streetlamp");
assert_eq!(
active(&h).as_deref(),
Some("streetlamp"),
"the member follows its entry through the rename"
);
assert_eq!(selected(&h).len(), 1, "and it is still the only member");
}
#[test]
fn deleting_an_earlier_entry_leaves_the_selection_on_its_own_member() {
let mut h = hook(vec![entry("a", "Decal"), entry("b", "Decal")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
select(&mut h, &["b"]);
let (g, i) = row_of(&h, "a");
h.apply_panel(PanelAction::OpenRowMenu(g, i), &mut world);
h.apply_panel(PanelAction::RowDelete, &mut world);
assert_eq!(selected(&h), vec!["b"]);
assert_eq!(h.handle_index(h.selection.active().unwrap()), Some(0));
}
#[test]
fn row_menu_delete_removes_the_entry() {
let mut h = hook(vec![entry("a", "Decal"), entry("b", "Decal")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
let (g, i) = row_of(&h, "a");
h.apply_panel(PanelAction::OpenRowMenu(g, i), &mut world);
h.apply_panel(PanelAction::RowDelete, &mut world);
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["args"]["$id"], "b");
assert!(h.dirty && h.assets.row_menu.is_none());
}
#[test]
fn edit_rename_to_a_duplicate_is_suffixed() {
let mut h = hook(vec![entry("a", "Decal"), entry("b", "Decal")]);
let mut world = world_with_fields();
let target = entry_target(&h, 1);
h.form.target = target;
h.form.selected_type = Some("Decal".to_string());
set_field(&mut world, form_panel::NAME_INPUT, "a");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries[1]["args"]["$id"], "a_1");
}
fn notices(h: &EditorHook) -> Vec<String> {
h.notifier
.stack()
.cards
.into_iter()
.map(|c| c.message)
.collect()
}
#[test]
fn edit_rename_moves_a_build_only_reference() {
let mut h = hook(vec![
entry("pause", "Screen"),
entry_with_args(
"card",
"Panel",
serde_json::json!({"screen": "pause", "title": "pause"}),
),
]);
let mut world = world_with_fields();
h.form.target = entry_target(&h, 0);
h.form.selected_type = Some("Screen".to_string());
set_field(&mut world, form_panel::NAME_INPUT, "paused");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries[0]["args"]["$id"], "paused");
assert_eq!(h.entries[1]["args"]["screen"], "paused");
assert_eq!(h.entries[1]["args"]["title"], "pause");
}
#[test]
fn blanking_a_referenced_id_keeps_it() {
let mut h = hook(vec![
entry("pause", "Screen"),
entry_with_args("card", "Panel", serde_json::json!({"screen": "pause"})),
entry("lone", "Screen"),
]);
let mut world = world_with_fields();
for at in [0, 2] {
h.form.target = entry_target(&h, at);
h.form.selected_type = Some("Screen".to_string());
set_field(&mut world, form_panel::NAME_INPUT, "");
h.apply_form(FormAction::Confirm, &mut world);
}
assert_eq!(h.entries[0]["args"]["$id"], "pause");
assert_eq!(h.entries[1]["args"]["screen"], "pause");
assert!(
notices(&h)
.iter()
.any(|n| n == "'pause' keeps its `$id`: 1 reference names it")
);
assert_eq!(declared_id(&h.entries[2]), None);
}
#[test]
fn confirm_add_with_a_blank_id_adds_it_anonymous() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.form.selected_type = Some("PointLight".to_string());
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), 1);
assert_eq!(declared_id(&h.entries[0]), None);
}
#[test]
fn confirm_add_makes_a_duplicate_name_unique() {
let mut h = hook(vec![entry("lamp", "PointLight")]);
let mut world = world_with_fields();
h.form.selected_type = Some("PointLight".to_string());
set_field(&mut world, form_panel::NAME_INPUT, "lamp");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(
h.entries[1]["args"]["$id"], "lamp_1",
"collision is suffixed"
);
}
#[test]
fn config_singleton_picker_edits_existing_else_adds() {
let mut h = hook(vec![serde_json::json!({
"type": "GraphicsConfig", "args": {"$id": "gfx"}
})]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.apply_panel(PanelAction::TogglePicker, &mut world);
let gi = h
.picker_options(&world)
.unwrap()
.iter()
.position(|o| o.name == "GraphicsConfig")
.expect("GraphicsConfig is offered in the picker");
h.apply_panel(PanelAction::PickOption(gi), &mut world);
assert!(h.form_open());
assert_eq!(
h.form.target,
entry_target(&h, 0),
"picking a present singleton edits it, not a new add"
);
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(
h.entries
.iter()
.filter(|e| e["type"] == "GraphicsConfig")
.count(),
1,
"the singleton was edited in place, never duplicated"
);
let mut h2 = hook(Vec::new());
let mut world2 = world_with_fields();
h2.open[PanelKey::Assets] = true;
h2.apply_panel(PanelAction::TogglePicker, &mut world2);
let wi = h2
.picker_options(&world2)
.unwrap()
.iter()
.position(|o| o.name == "Window")
.expect("Window is offered in the picker");
h2.apply_panel(PanelAction::PickOption(wi), &mut world2);
assert!(h2.form_open());
assert!(
(h2.form.target == FormTarget::New),
"no existing Window -> an add form"
);
h2.apply_form(FormAction::Confirm, &mut world2);
assert_eq!(
h2.entries.iter().filter(|e| e["type"] == "Window").count(),
1,
"the missing singleton was added"
);
}
#[test]
fn cancel_form_returns_to_the_list_without_adding() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.form.selected_type = Some("Decal".to_string());
h.apply_form(FormAction::Close, &mut world);
assert!(!h.form_open());
assert!(h.form.selected_type.is_none() && (h.form.target == FormTarget::New));
assert!(h.entries.is_empty() && !h.dirty);
}
#[test]
fn picker_lists_types_alphabetically() {
let mut h = hook(Vec::new());
let world = world_with_fields();
h.assets.picker_open = true;
let opts = h.picker_options(&world).unwrap();
let names: Vec<&str> = opts.iter().map(|o| o.name.as_str()).collect();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted, "the picker is alphabetized ascending");
assert_eq!(
opts.len(),
assets_panel::picker_types().count(),
"every offered type shown (addables + config singletons)"
);
let pos = |t: &str| opts.iter().position(|o| o.name == t).unwrap();
assert!(pos("AudioCue") < pos("Sprite"));
assert!(pos("AppConfig") < pos("AudioCue"));
}
#[test]
fn close_overlays_dismisses_the_picker_and_row_menu() {
let mut h = hook(vec![entry("a", "Decal")]);
let mut world = world_with_fields();
h.assets.picker_open = true;
h.assets.row_menu = Some(h.handle_for("a"));
h.apply_panel(PanelAction::CloseOverlays, &mut world);
assert!(!h.assets.picker_open);
assert!(h.assets.row_menu.is_none());
}
#[test]
fn clicking_a_list_row_opens_its_edit_form() {
let mut h = hook(vec![entry("lamp", "PointLight")]);
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
let vp = [1280.0, 720.0];
let po = h.origin(PanelKey::Assets, vp);
let row = assets_panel::row_rect(po, assets_panel::PANEL_W, 1);
let mut world = World::new();
inject::editor_hud(&mut world);
world.add_component(FrameInput {
viewport: vp,
mouse_x: row[0] + 60.0,
mouse_y: row[1] + 10.0,
left_click: true,
left_button_down: true,
..Default::default()
});
h.tick(&mut world);
assert!(h.form_open(), "the row click opened the form");
assert_eq!(h.form.target, entry_target(&h, 0));
assert_eq!(h.form.selected_type.as_deref(), Some("PointLight"));
assert_eq!(
widget::field_text(&world, form_panel::NAME_INPUT),
"lamp",
"the name heading is seeded from the entry"
);
}
#[test]
fn deleting_an_unrelated_entry_leaves_the_open_form_on_its_own() {
let mut h = hook(vec![entry("a", "Decal"), entry("b", "Decal")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
let target = entry_target(&h, 1);
h.open_form(&mut world, "Decal".to_string(), target);
let (g, i) = row_of(&h, "a");
h.apply_panel(PanelAction::OpenRowMenu(g, i), &mut world);
h.apply_panel(PanelAction::RowDelete, &mut world);
assert!(h.form_open(), "the form survives an unrelated delete");
assert_eq!(
h.form.target,
entry_target(&h, 0),
"the same entry, now at 0"
);
set_field(&mut world, form_panel::NAME_INPUT, "b2");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["args"]["$id"], "b2");
let mut h2 = hook(vec![entry("a", "Decal")]);
let mut world2 = world_with_fields();
h2.open[PanelKey::Assets] = true;
seed_tree(&mut h2, Vec::new());
let target = entry_target(&h2, 0);
h2.open_form(&mut world2, "Decal".to_string(), target);
let (g2, i2) = row_of(&h2, "a");
h2.apply_panel(PanelAction::OpenRowMenu(g2, i2), &mut world2);
h2.apply_panel(PanelAction::RowDelete, &mut world2);
assert!(!h2.form_open(), "deleting the edited entry closes its form");
}
#[test]
fn add_form_writes_edited_arg_values() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.apply_panel(PanelAction::TogglePicker, &mut world);
let ty = "PointLight".to_string();
let idx = h
.picker_options(&world)
.unwrap()
.iter()
.position(|o| o.name == ty)
.expect("PointLight is offered");
h.apply_panel(PanelAction::PickOption(idx), &mut world);
assert!(h.form_open());
assert!(!h.form.fields.is_empty(), "the type exposes arg fields");
let (j, key) = h
.form
.fields
.iter()
.enumerate()
.find(|(_, f)| matches!(f.kind, form::FieldKind::Float))
.map(|(j, f)| (j, f.key.clone()))
.expect("a float arg field");
set_field(&mut world, form_panel::form_input(j), "3.5");
set_field(&mut world, form_panel::NAME_INPUT, "lamp");
h.apply_form(FormAction::Confirm, &mut world);
assert!(!h.form_open());
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["args"]["$id"], "lamp");
assert_eq!(h.entries[0]["type"], ty.as_str());
assert_eq!(
h.entries[0]["args"][&key].as_f64(),
Some(3.5),
"the edited float persisted into args"
);
}
#[test]
fn add_form_writes_an_edited_color_vector() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "VolumetricFog".to_string(), FormTarget::New);
let (j, key) = h
.form
.fields
.iter()
.enumerate()
.find(|(_, f)| matches!(f.kind, form::FieldKind::Vec { color: true, .. }))
.map(|(j, f)| (j, f.key.clone()))
.expect("a color vector field");
set_field(&mut world, form_panel::form_input(j), "0.1, 0.2, 0.3");
set_field(&mut world, form_panel::NAME_INPUT, "fog");
h.apply_form(FormAction::Confirm, &mut world);
assert!(!h.form_open());
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["type"], "VolumetricFog");
assert_eq!(
h.entries[0]["args"][&key],
serde_json::json!([0.1, 0.2, 0.3]),
"the edited color persisted as a numeric array"
);
}
#[test]
fn add_form_writes_a_nested_object_field() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "Camera3D".to_string(), FormTarget::New);
let j = h
.form
.fields
.iter()
.position(|f| f.key == "controller.move_speed")
.expect("the nested controller.move_speed field is offered");
assert!(matches!(h.form.fields[j].kind, form::FieldKind::Float));
set_field(&mut world, form_panel::form_input(j), "12.5");
set_field(&mut world, form_panel::NAME_INPUT, "cam");
h.apply_form(FormAction::Confirm, &mut world);
let cam = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "cam")
.expect("the camera was added");
assert_eq!(cam["type"], "Camera3D");
assert_eq!(
cam["args"]["controller"]["move_speed"].as_f64(),
Some(12.5),
"the nested edit persisted into args.controller.move_speed"
);
}
#[test]
fn add_form_writes_a_key_and_a_picked_action_for_a_new_type() {
let mut h = hook(vec![entry("pause", "Screen")]);
let mut world = world_with_fields();
h.open_form(&mut world, "KeyBinding".to_string(), FormTarget::New);
let field_pos = |k: &str| {
h.form
.fields
.iter()
.position(|f| f.key == k)
.unwrap_or_else(|| panic!("{k} field present"))
};
let (key_j, action_j) = (field_pos("key"), field_pos("action"));
assert!(matches!(h.form.fields[key_j].kind, form::FieldKind::Str));
let action = &mut h.form.fields[action_j];
assert_eq!(action.variants[action.variant_idx], "(none)");
action.variant_idx = action
.variants
.iter()
.position(|v| v == "toggle pause")
.expect("the Screen is offered");
set_field(&mut world, form_panel::form_input(key_j), "Escape");
set_field(&mut world, form_panel::NAME_INPUT, "pause_key");
h.apply_form(FormAction::Confirm, &mut world);
assert!(!h.form_open());
assert_eq!(h.entries.len(), 2);
assert_eq!(h.entries[1]["type"], "KeyBinding");
assert_eq!(h.entries[1]["args"]["key"], "Escape");
assert_eq!(
h.entries[1]["args"]["action"],
serde_json::json!({"toggle": "pause"})
);
}
#[test]
fn add_form_cycles_and_persists_an_enum_field() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "Sprite".to_string(), FormTarget::New);
let idx = h
.form
.fields
.iter()
.position(|f| f.key == "fit")
.expect("fit enum field");
assert!(matches!(h.form.fields[idx].kind, form::FieldKind::Enum));
let n = h.form.fields[idx].variants.len();
let start = h.form.fields[idx].variant_idx;
h.apply_form(FormAction::CycleField(idx), &mut world);
let picked = h.form.fields[idx].variants[(start + 1) % n].clone();
assert_ne!(
picked, h.form.fields[idx].variants[start],
"cycled to a new value"
);
set_field(&mut world, form_panel::NAME_INPUT, "spr");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["type"], "Sprite");
assert_eq!(
h.entries[0]["args"]["fit"], picked,
"the cycled enum variant persisted into args"
);
}
#[test]
fn add_form_ref_field_offers_every_target_type() {
let mut h = hook(vec![
entry("door", "Prop"),
entry("pivot", "SkyRotation"),
entry("grass_tex", "Texture"),
]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.open_form(&mut world, "Prop".to_string(), FormTarget::New);
let parent = h
.form
.fields
.iter()
.find(|f| f.key == "parent")
.expect("parent ref field");
assert_eq!(parent.variants, vec![form::NONE_LABEL, "door", "pivot"]);
}
#[test]
fn add_form_ref_field_offers_and_persists_an_existing_asset() {
let mut h = hook(vec![
entry("grass_tex", "Texture"),
entry("stone_tex", "Texture"),
]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.open_form(&mut world, "Decal".to_string(), FormTarget::New);
let idx = h
.form
.fields
.iter()
.position(|f| f.key == "texture")
.expect("texture ref field");
assert!(
matches!(h.form.fields[idx].kind, form::FieldKind::Ref { targets } if targets == ["Texture"])
);
assert_eq!(
h.form.fields[idx].variants,
vec![form::NONE_LABEL, "grass_tex", "stone_tex"],
"options are (none) + the world's Textures"
);
assert_eq!(h.form.fields[idx].variant_idx, 0, "starts at (none)");
h.apply_form(FormAction::CycleField(idx), &mut world);
assert_eq!(
h.form.fields[idx].variants[h.form.fields[idx].variant_idx],
"grass_tex"
);
set_field(&mut world, form_panel::NAME_INPUT, "splat");
h.apply_form(FormAction::Confirm, &mut world);
let decal = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "splat")
.expect("the decal was added");
assert_eq!(decal["type"], "Decal");
assert_eq!(
decal["args"]["texture"], "grass_tex",
"the reference persisted as the asset's name"
);
}
#[test]
fn add_form_ref_field_dropdown_picks_and_persists() {
let mut entries = Vec::new();
for i in 0..(form_panel::CYCLE_MAX + 3) {
entries.push(entry(&format!("tex_{i}"), "Texture"));
}
let mut h = hook(entries);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.open_form(&mut world, "Decal".to_string(), FormTarget::New);
let idx = h
.form
.fields
.iter()
.position(|f| f.key == "texture")
.expect("texture ref field");
assert!(h.form.fields[idx].variants.len() > form_panel::CYCLE_MAX);
h.apply_form(FormAction::OpenFieldDropdown(idx), &mut world);
assert_eq!(h.form.field_dropdown, Some(idx), "the dropdown opened");
let picked = h.form.fields[idx].variants[3].clone();
h.apply_form(FormAction::PickFieldOption(3), &mut world);
assert!(
h.form.field_dropdown.is_none(),
"picking closes the dropdown"
);
assert_eq!(h.form.fields[idx].variant_idx, 3, "the option was selected");
set_field(&mut world, form_panel::NAME_INPUT, "splat");
h.apply_form(FormAction::Confirm, &mut world);
let decal = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "splat")
.unwrap();
assert_eq!(
decal["args"]["texture"], picked,
"the dropdown-picked reference persisted as the asset's name"
);
}
#[test]
fn field_dropdown_toggles_and_close_overlays_dismisses_it() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.form.selected_type = Some("Decal".to_string());
h.apply_form(FormAction::OpenFieldDropdown(0), &mut world);
assert_eq!(h.form.field_dropdown, Some(0));
h.apply_form(FormAction::OpenFieldDropdown(0), &mut world);
assert!(h.form.field_dropdown.is_none(), "a second click closes it");
h.apply_form(FormAction::OpenFieldDropdown(0), &mut world);
h.apply_form(FormAction::CloseOverlays, &mut world);
assert!(
h.form.field_dropdown.is_none(),
"CloseOverlays dismisses it"
);
}
#[test]
fn scrolling_advances_an_open_field_dropdown() {
let mut entries = Vec::new();
for i in 0..(form_panel::MAX_DROP_ROWS + 4) {
entries.push(entry(&format!("tex_{i}"), "Texture"));
}
let mut h = hook(entries);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
h.open_form(&mut world, "Decal".to_string(), FormTarget::New);
let idx = h
.form
.fields
.iter()
.position(|f| f.key == "texture")
.expect("texture ref field");
h.apply_form(FormAction::OpenFieldDropdown(idx), &mut world);
assert_eq!(h.form.field_dropdown_scroll, 0);
h.scroll_form(1.0, &mut world);
assert_eq!(
h.form.field_dropdown_scroll, 1,
"wheel down advances the dropdown"
);
h.scroll_form(-1.0, &mut world);
assert_eq!(h.form.field_dropdown_scroll, 0, "wheel up rewinds it");
for _ in 0..50 {
h.scroll_form(1.0, &mut world);
}
let total = h.form.fields[idx].variants.len();
assert_eq!(
h.form.field_dropdown_scroll,
total - form_panel::MAX_DROP_ROWS,
"scroll clamps to the last full page"
);
}
#[test]
fn add_form_grows_an_array_and_edits_the_new_element() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "WaterSurface".to_string(), FormTarget::New);
let header = |h: &EditorHook| {
h.form
.fields
.iter()
.position(|f| f.key == "waves")
.expect("waves array header")
};
let hj = header(&h);
assert!(matches!(h.form.fields[hj].kind, form::FieldKind::Array));
assert_eq!(h.form.fields[hj].variant_idx, 1, "one default wave");
h.apply_form(FormAction::AddArrayElement(hj), &mut world);
assert_eq!(
h.form.fields[header(&h)].variant_idx,
2,
"grew to two waves"
);
let ej = h
.form
.fields
.iter()
.position(|f| f.key == "waves.1.amplitude")
.expect("the second wave's amplitude field");
set_field(&mut world, form_panel::form_input(ej), "4.5");
set_field(&mut world, form_panel::NAME_INPUT, "sea");
h.apply_form(FormAction::Confirm, &mut world);
let ws = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "sea")
.expect("the water surface was added");
assert_eq!(ws["type"], "WaterSurface");
assert_eq!(
ws["args"]["waves"].as_array().map(Vec::len),
Some(2),
"the grown array persisted with two waves"
);
assert_eq!(
ws["args"]["waves"][1]["amplitude"].as_f64(),
Some(4.5),
"the edited new-element value persisted"
);
}
#[test]
fn add_form_removes_an_array_element() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "WaterSurface".to_string(), FormTarget::New);
let hj = h.form.fields.iter().position(|f| f.key == "waves").unwrap();
h.apply_form(FormAction::AddArrayElement(hj), &mut world);
let hj = h.form.fields.iter().position(|f| f.key == "waves").unwrap();
assert_eq!(h.form.fields[hj].variant_idx, 2);
h.apply_form(FormAction::RemoveArrayElement(hj), &mut world);
let hj = h.form.fields.iter().position(|f| f.key == "waves").unwrap();
assert_eq!(h.form.fields[hj].variant_idx, 1, "shrank back to one wave");
set_field(&mut world, form_panel::NAME_INPUT, "pond");
h.apply_form(FormAction::Confirm, &mut world);
let ws = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "pond")
.unwrap();
assert_eq!(ws["args"]["waves"].as_array().map(Vec::len), Some(1));
}
#[test]
fn form_discloses_a_vector_and_edits_one_element() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "PointLight".to_string(), FormTarget::New);
let pos = |h: &EditorHook| {
h.form
.fields
.iter()
.position(|f| f.key == "position")
.expect("a position vector field")
};
assert!(
h.form
.fields
.iter()
.all(|f| !f.key.starts_with("position."))
);
h.apply_form(FormAction::ToggleVecExpand(pos(&h)), &mut world);
assert!(h.form.vec_expanded.contains("position"));
let yj = h
.form
.fields
.iter()
.position(|f| f.key == "position.1")
.expect("the y element leaf");
let slot = visible_slot(yj, h.form.scroll, h.form_window()).expect("y leaf visible");
set_field(&mut world, form_panel::form_input(slot), "4.5");
set_field(&mut world, form_panel::NAME_INPUT, "lamp");
h.apply_form(FormAction::Confirm, &mut world);
let lamp = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "lamp")
.unwrap();
assert_eq!(
lamp["args"]["position"].as_array().map(Vec::len),
Some(3),
"the vector kept its length"
);
assert_eq!(lamp["args"]["position"][1].as_f64(), Some(4.5));
}
#[test]
fn collapsing_a_vector_keeps_its_element_edits() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "PointLight".to_string(), FormTarget::New);
let pj = h
.form
.fields
.iter()
.position(|f| f.key == "position")
.unwrap();
h.apply_form(FormAction::ToggleVecExpand(pj), &mut world);
let xj = h
.form
.fields
.iter()
.position(|f| f.key == "position.0")
.unwrap();
let slot = visible_slot(xj, h.form.scroll, h.form_window()).unwrap();
set_field(&mut world, form_panel::form_input(slot), "2.0");
let pj = h
.form
.fields
.iter()
.position(|f| f.key == "position")
.unwrap();
h.apply_form(FormAction::ToggleVecExpand(pj), &mut world);
assert!(!h.form.vec_expanded.contains("position"));
assert!(
h.form
.fields
.iter()
.all(|f| !f.key.starts_with("position."))
);
set_field(&mut world, form_panel::NAME_INPUT, "lamp");
h.apply_form(FormAction::Confirm, &mut world);
let lamp = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "lamp")
.unwrap();
assert_eq!(lamp["args"]["position"][0].as_f64(), Some(2.0));
}
#[test]
fn add_form_scrolls_to_and_edits_an_off_window_field() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "WaterSurface".to_string(), FormTarget::New);
assert!(
h.form.fields.len() > form::FIELD_POOL,
"WaterSurface overflows the control pool"
);
let rj = h
.form
.fields
.iter()
.position(|f| f.key == "roughness")
.expect("a roughness field");
assert!(
visible_slot(rj, h.form.scroll, h.form_window()).is_none(),
"roughness starts past the visible window"
);
for _ in 0..h.form.fields.len() {
h.scroll_form(1.0, &mut world);
}
let slot =
visible_slot(rj, h.form.scroll, h.form_window()).expect("roughness scrolled into view");
set_field(&mut world, form_panel::form_input(slot), "0.9");
set_field(&mut world, form_panel::NAME_INPUT, "sea");
h.apply_form(FormAction::Confirm, &mut world);
let ws = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "sea")
.expect("the water surface was added");
assert_eq!(
ws["args"]["roughness"].as_f64(),
Some(0.9),
"the off-window field's edit persisted after scrolling to it"
);
assert_eq!(
ws["args"]["extent"],
form::base_args("WaterSurface")["extent"],
"a scrolled-away field kept its value"
);
}
#[test]
fn add_form_ref_field_defaults_to_none() {
let mut h = hook(vec![entry("grass_tex", "Texture")]);
let mut world = world_with_fields();
h.open_form(&mut world, "Decal".to_string(), FormTarget::New);
set_field(&mut world, form_panel::NAME_INPUT, "bare");
h.apply_form(FormAction::Confirm, &mut world);
let decal = h
.entries
.iter()
.find(|e| e["args"]["$id"] == "bare")
.unwrap();
assert_eq!(decal["args"]["texture"], serde_json::Value::Null);
}
#[test]
fn invalid_arg_keeps_the_form_open_with_an_error() {
let mut h = hook(Vec::new());
let mut world = world_with_fields();
h.open_form(&mut world, "Font".to_string(), FormTarget::New);
let j = h
.form
.fields
.iter()
.position(|f| f.key == "size_px")
.expect("size_px field present");
assert!(matches!(h.form.fields[j].kind, form::FieldKind::Int));
set_field(&mut world, form_panel::form_input(j), "-5");
set_field(&mut world, form_panel::NAME_INPUT, "myfont");
h.apply_form(FormAction::Confirm, &mut world);
assert!(h.form_open(), "the form stays open on invalid input");
assert!(h.form.error.is_some(), "an error message is shown");
assert!(h.entries.is_empty(), "nothing invalid was committed");
}
#[test]
fn toggling_the_assets_panel_keeps_the_open_form_state() {
let mut h = hook(vec![entry("lamp", "PointLight")]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
let target = entry_target(&h, 0);
h.open_form(&mut world, "PointLight".to_string(), target);
assert!(h.form_open() && h.form.target == entry_target(&h, 0));
h.toggle_view_row(0, &mut world);
assert!(!h.open[PanelKey::Assets]);
assert!(
h.form_open(),
"the form is kept when the panel is toggled off"
);
assert_eq!(
h.form.target,
entry_target(&h, 0),
"the browse selection is kept"
);
h.toggle_view_row(0, &mut world);
assert!(h.open[PanelKey::Assets] && h.form_open());
assert_eq!(h.form.target, entry_target(&h, 0));
}
#[test]
fn a_hidden_assets_panel_hides_the_form_elements() {
let mut world = World::new();
inject::editor_hud(&mut world);
world.add_component(FrameInput {
viewport: [1280.0, 720.0],
..Default::default()
});
let mut h = hook(vec![entry("lamp", "PointLight")]);
h.open[PanelKey::Assets] = true;
let target = entry_target(&h, 0);
h.open_form(&mut world, "PointLight".to_string(), target);
let form_shown = |w: &World| w.get_by_id::<Sprite>(form_panel::EDIT_BG).unwrap().visible;
h.tick(&mut world);
assert!(form_shown(&world), "form shown while the panel is open");
h.toggle_view_row(0, &mut world);
h.tick(&mut world);
assert!(!form_shown(&world), "form elements hidden when toggled off");
assert!(h.form_open(), "but the form state is retained");
h.toggle_view_row(0, &mut world);
h.tick(&mut world);
assert!(form_shown(&world), "form shown again on toggle-on");
}
#[test]
fn edit_form_seeds_and_updates_existing_args() {
let mut h = hook(vec![serde_json::json!({
"type": "PointLight", "args": {"$id": "lamp"}
})]);
let mut world = world_with_fields();
h.open[PanelKey::Assets] = true;
seed_tree(&mut h, Vec::new());
click_row(&mut h, "lamp", &mut world);
assert_eq!(h.form.target, entry_target(&h, 0));
assert!(!h.form.fields.is_empty());
assert_eq!(widget::field_text(&world, form_panel::NAME_INPUT), "lamp");
let (j, key) = h
.form
.fields
.iter()
.enumerate()
.find(|(_, f)| matches!(f.kind, form::FieldKind::Float))
.map(|(j, f)| (j, f.key.clone()))
.expect("a float arg field");
set_field(&mut world, form_panel::form_input(j), "9.0");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries.len(), 1, "edited in place");
assert_eq!(h.entries[0]["args"][&key].as_f64(), Some(9.0));
}
#[test]
fn renaming_through_the_form_moves_the_references_with_it() {
let mut h = hook(vec![
entry("mat", "Material"),
entry("stone", "Material"),
entry_with_args(
"box",
"Prop",
serde_json::json!({"mesh": "cube", "material": "mat"}),
),
entry_with_args(
"cube",
"ProceduralMesh",
serde_json::json!({"generator": "box"}),
),
]);
let mut world = world_with_fields();
let target = entry_target(&h, 0);
h.open_form(&mut world, "Material".to_string(), target);
set_field(&mut world, form_panel::NAME_INPUT, "stone");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(h.entries[0]["args"]["$id"], "stone_1");
assert_eq!(h.entries[2]["args"]["material"], "stone_1");
let target = entry_target(&h, 3);
h.open_form(&mut world, "ProceduralMesh".to_string(), target);
set_field(&mut world, form_panel::NAME_INPUT, "crate");
h.apply_form(FormAction::Confirm, &mut world);
assert_eq!(
h.entries[2]["args"]["mesh"], "crate",
"a structured reference"
);
h.undo(&mut world);
assert_eq!(h.entries[3]["args"]["$id"], "cube");
assert_eq!(h.entries[2]["args"]["mesh"], "cube");
h.undo(&mut world);
assert_eq!(h.entries[0]["args"]["$id"], "mat");
assert_eq!(h.entries[2]["args"]["material"], "mat");
assert!(!h.can_undo(), "each rename was one step");
}