use concinnity_cook::authoring::world::parse_world_jsonl;
use concinnity_core::components::FrameInput;
use concinnity_core::components::KeyEvent;
use concinnity_core::components::KeyMods;
use concinnity_core::components::KeyPress;
use concinnity_core::components::TextLabel;
use concinnity_core::ecs::World;
use super::fixtures::{entry, hook, set_field, world_with_fields, world_with_input};
use crate::editor::hook::{EditorHook, FormTarget};
use crate::frame_hook::FrameHook;
use crate::editor::hud::HudAction;
use crate::editor::panels::form_panel::{self, FormAction};
use crate::editor::panels::template_panel::TemplateAction;
use crate::editor::hook::tests::fixtures::entry_target;
use crate::editor::panels::registry::PanelKey;
use crate::editor::sim;
use crate::test_support::isolate_state_dir;
#[test]
fn entry_changes_request_a_preview_rebuild() {
let mut h = hook(Vec::new());
h.apply_top(HudAction::ToggleView, &mut World::new());
assert!(
!h.rebuild_preview && !h.dirty,
"a view toggle is not an entry change"
);
h.open_template_detail(0);
h.apply_template_detail(TemplateAction::Apply);
assert!(
h.rebuild_preview && h.dirty,
"applying a template updates the live preview and marks unsaved"
);
}
#[test]
fn build_preview_world_renders_from_in_memory_entries() {
let _guard = crate::test_support::lock();
isolate_state_dir();
let h = hook(vec![
serde_json::json!({"type":"Camera3D","args":{"$id":"cam"}}),
serde_json::json!({"type":"Room","args":{"$id":"room"}}),
]);
assert!(
h.build_preview_world()
.expect("authored entries build")
.0
.renders(),
"authored renderable entries render without disk"
);
let h = hook(Vec::new());
assert!(
h.build_preview_world()
.expect("empty world seeds")
.0
.renders(),
"an empty world is seeded so it still renders"
);
}
#[test]
fn a_live_edit_reaches_a_really_cooked_world() {
let _guard = crate::test_support::lock();
isolate_state_dir();
let mut h = hook(vec![
serde_json::json!({"type":"Camera3D","args":{"$id":"cam"}}),
serde_json::json!({"type":"Room","args":{"$id":"room"}}),
serde_json::json!({"type":"TextLabel","args":{"$id":"hint","content":"before"}}),
]);
let (mut world, shadows) = h.build_preview_world().expect("the world builds");
h.world_shadows = Some(shadows);
assert_eq!(
world
.query::<TextLabel>()
.next()
.expect("the label is in the world")
.content,
"before"
);
h.entries[2]["args"]["content"] = serde_json::json!("after");
h.mark_changed();
assert!(
!h.refresh_preview(&mut world),
"a TextLabel field is written into the running world"
);
assert_eq!(
world.query::<TextLabel>().next().unwrap().content,
"after",
"and the running component carries the edit"
);
h.entries.push(serde_json::json!({
"type":"TextLabel","args":{"$id":"hint2","content":"new"}
}));
h.mark_changed();
assert!(h.refresh_preview(&mut world), "a new line rebuilds");
}
#[test]
fn write_jsonl_persists_entries_atomically() {
let tree = concinnity_testing::TempTree::new();
let path = tree.join("world.jsonl");
let path_str = path.to_str().unwrap().to_string();
let mut h = hook(vec![serde_json::json!({
"type": "GraphicsConfig", "args": {"$id": "scene"}
})]);
h.world_path = path_str.clone();
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);
h.write_jsonl().unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let parsed = parse_world_jsonl(&content).unwrap();
assert_eq!(parsed.len(), 2, "both entries written, one line each");
assert_eq!(parsed[1]["args"]["$id"], "lamp");
assert!(!std::path::Path::new(&format!("{path_str}.tmp")).exists());
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_jsonl_creates_the_worlds_directory() {
let tree = concinnity_testing::TempTree::new();
let path = tree.join("worlds").join("world.jsonl");
let mut h = hook(vec![serde_json::json!({
"type": "GraphicsConfig", "args": {"$id": "scene"}
})]);
h.world_path = path.to_str().unwrap().to_string();
h.write_jsonl().expect("the write creates its directory");
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(parse_world_jsonl(&content).unwrap().len(), 1);
}
#[test]
fn a_broken_world_reports_its_error_in_the_status_line() {
let _guard = crate::test_support::lock();
isolate_state_dir();
let mut h = hook(vec![serde_json::json!({
"type": "NotARealAssetType", "args": {"$id": "oops"}
})]);
h.open[PanelKey::Assets] = true;
h.refresh_tree_if_needed();
assert!(h.assets.groups.is_empty());
let status = h.assets.status.as_deref().expect("the failure surfaces");
assert!(status.contains("NotARealAssetType"), "{status}");
}
#[test]
fn undo_reverts_a_committed_edit_and_redo_replays_it() {
let mut world = World::new();
let mut h = hook(vec![entry("a", "Sprite")]);
assert!(!h.hud_state().undo && !h.hud_state().redo);
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
assert!(h.dirty && h.hud_state().undo);
h.undo(&mut world);
assert_eq!(*h.entries, [entry("a", "Sprite")]);
assert!(!h.dirty, "back at the on-disk list: Save chip clears");
assert!(
h.rebuild_preview,
"the restored list drives the live preview"
);
assert!(h.hud_state().redo);
h.redo(&mut world);
assert_eq!(*h.entries, [entry("a", "Sprite"), entry("b", "Sprite")]);
assert!(h.dirty, "the replayed edit is unsaved again");
}
#[test]
fn an_edit_after_undo_drops_the_redo_branch() {
let mut world = World::new();
let mut h = hook(Vec::new());
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
h.undo(&mut world);
assert!(h.hud_state().redo);
h.entries.push(entry("c", "Sprite"));
h.mark_changed();
assert!(!h.hud_state().redo, "the new edit invalidates redo");
h.undo(&mut world);
assert!(h.entries.is_empty());
}
#[test]
fn a_no_change_mark_records_no_undo_step() {
let mut h = hook(vec![entry("a", "Sprite")]);
h.mark_changed();
assert!(!h.hud_state().undo, "nothing changed, nothing to undo");
}
#[test]
fn undo_drops_entry_indexed_ui_state() {
let mut world = World::new();
let mut h = hook(vec![entry("a", "Sprite")]);
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
h.form.selected_type = Some("Sprite".to_string());
let target = entry_target(&h, 1);
h.form.target = target;
h.assets.row_menu = Some(h.handle_for("b"));
h.undo(&mut world);
assert_eq!(
h.form.target,
FormTarget::New,
"the form no longer targets a live row"
);
assert_eq!(h.form.selected_type, None);
assert_eq!(h.assets.row_menu, None);
}
#[test]
fn ctrl_z_y_step_history_unless_typing_or_playing() {
use concinnity_core::components::InputKey;
let step = |h: &mut EditorHook, key: InputKey| {
let mut world = world_with_input(FrameInput {
viewport: [1280.0, 720.0],
ctrl: true,
key_events: vec![KeyEvent::Press(KeyPress::new(key, KeyMods::CTRL))],
..Default::default()
});
h.tick(&mut world);
};
let mut h = hook(Vec::new());
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
crate::editor::hook::tests::fixtures::focus_story(&mut h);
step(&mut h, InputKey::Z);
assert_eq!(
h.entries.len(),
1,
"suppressed while a text field is focused"
);
h.story.focus = false;
h.sim.state = sim::SimState::Playing;
step(&mut h, InputKey::Z);
assert_eq!(h.entries.len(), 1, "suppressed in play mode");
h.sim.state = sim::SimState::Stopped;
step(&mut h, InputKey::Z);
assert!(h.entries.is_empty(), "Ctrl+Z undoes the edit");
step(&mut h, InputKey::Y);
assert_eq!(h.entries.len(), 1, "Ctrl+Y redoes it");
}
#[test]
fn save_writes_the_world_file_and_no_build_output() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = dir.path().join(".concinnity");
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir.path()).with_build(&build_root),
);
let world_path = dir.path().join("worlds").join("world.jsonl");
let entries = vec![serde_json::json!({"type":"PhysicsConfig","args":{"$id":"phys"}})];
let mut h = EditorHook::new(world_path.to_string_lossy().into_owned(), entries.clone());
h.entries
.push(serde_json::json!({"type":"TextLabel","args":{"$id":"hint"}}));
h.mark_changed();
h.save();
let written = std::fs::read_to_string(&world_path).expect("the world file was created");
assert_eq!(
parse_world_jsonl(&written).unwrap(),
*h.entries,
"the working entries are what landed on disk"
);
assert!(!h.dirty, "a written save clears the unsaved chip");
assert_eq!(h.saved, h.entries, "and re-baselines dirty tracking");
assert!(
!build_root.exists(),
"no blobs, no lock, no build root at all"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn a_failed_save_leaves_the_world_dirty() {
let dir = concinnity_testing::TempTree::new();
let blocker = dir.path().join("blocker");
std::fs::write(&blocker, b"not a directory").unwrap();
let world_path = blocker.join("worlds").join("world.jsonl");
let mut h = EditorHook::new(world_path.to_string_lossy().into_owned(), Vec::new());
h.entries
.push(serde_json::json!({"type":"TextLabel","args":{"$id":"hint"}}));
h.mark_changed();
h.save();
assert!(h.dirty, "the world stays unsaved");
assert!(h.saved.is_empty(), "and the saved baseline does not move");
}
#[test]
fn dirty_tracks_the_saved_list_across_history_jumps() {
let mut world = World::new();
let mut h = hook(Vec::new());
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
h.dirty = false;
h.saved = h.entries.clone();
h.undo(&mut world);
assert!(h.dirty, "behind the saved list is an unsaved state");
h.redo(&mut world);
assert!(!h.dirty, "redo back to the saved list clears the chip");
}
fn hook_with_an_include(tree: &concinnity_testing::TempTree) -> EditorHook {
tree.write(
"lights.jsonl",
"[\"PointLight\",{\"$id\":\"lamp\"}]\n[\"PointLight\"]\n",
);
let world = tree.write(
"world.jsonl",
"[\"PointLight\"]\n[\"Include\",{\"path\":\"lights.jsonl\"}]\n[\"Camera3D\",{\"$id\":\"mine\"}]\n",
);
let entries = crate::editor::worlds::files::read_entries(&world).expect("the world reads");
EditorHook::new(world.to_str().unwrap().to_string(), entries)
}
#[test]
fn included_entries_take_the_labels_the_build_gives_them() {
let tree = concinnity_testing::TempTree::new();
let h = hook_with_an_include(&tree);
let handles = concinnity_cook::authoring::world::entry_handles(&h.entries);
assert_eq!(
handles.into_iter().flatten().collect::<Vec<_>>(),
["PointLight#0", "Include#0", "lamp", "PointLight#1", "mine"]
);
}
#[test]
fn an_edit_to_an_included_entry_or_include_line_is_refused() {
let tree = concinnity_testing::TempTree::new();
let mut h = hook_with_an_include(&tree);
let before = h.entries.clone();
h.entries[2]["args"]["intensity"] = serde_json::json!(5.0);
h.mark_changed();
assert_eq!(h.entries, before, "the included light is put back");
assert!(!h.dirty && !h.can_undo(), "nothing was recorded");
assert!(
h.rebuild_required,
"the preview is rebuilt from the kept list"
);
h.remove_entry_at(1);
assert_eq!(h.entries, before, "the Include line stays");
assert!(!h.dirty);
}
#[test]
fn a_save_writes_only_the_world_file_and_keeps_the_include_line() {
let tree = concinnity_testing::TempTree::new();
let mut h = hook_with_an_include(&tree);
h.entries[4]["args"]["fov_y_degrees"] = serde_json::json!(60.0);
h.mark_changed();
assert!(h.dirty);
h.write_jsonl().unwrap();
assert_eq!(
std::fs::read_to_string(tree.join("world.jsonl")).unwrap(),
"[\"PointLight\",{}]\n[\"Include\",{\"path\":\"lights.jsonl\"}]\n[\"Camera3D\",{\"$id\":\"mine\",\"fov_y_degrees\":60.0}]\n"
);
assert_eq!(
std::fs::read_to_string(tree.join("lights.jsonl")).unwrap(),
"[\"PointLight\",{\"$id\":\"lamp\"}]\n[\"PointLight\"]\n",
"the included file is not the editor's to write"
);
}
#[test]
fn the_preview_builds_the_included_entries() {
let _guard = crate::test_support::lock();
isolate_state_dir();
let tree = concinnity_testing::TempTree::new();
let h = hook_with_an_include(&tree);
std::fs::remove_file(tree.join("lights.jsonl")).unwrap();
let (world, _) = h.build_preview_world().expect("the world builds");
assert_eq!(
world
.query::<concinnity_core::components::PointLight>()
.count(),
3
);
}