use super::*;
use crate::components::TextInput;
const VP: [f32; 2] = [1280.0, 720.0];
pub(super) fn open_project(dir: &std::path::Path) {
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir).with_cache(
concinnity_testing::shared_cache_dir("concinnity-dev-tests-cache"),
),
);
}
pub(super) fn entry(name: &str) -> serde_json::Value {
serde_json::json!({"name": name, "type": "Prop", "args": {}})
}
pub(super) fn write_world(
dir: &std::path::Path,
name: &str,
entries: &[serde_json::Value],
at_secs: u64,
) -> std::path::PathBuf {
std::fs::create_dir_all(dir).unwrap();
let path = dir.join(format!("{name}.jsonl"));
std::fs::write(&path, crate::world::write_world_jsonl(entries).unwrap()).unwrap();
let file = std::fs::File::options().write(true).open(&path).unwrap();
file.set_modified(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(at_secs))
.unwrap();
path
}
pub(super) fn hook_at(path: &std::path::Path, entries: Vec<serde_json::Value>) -> EditorHook {
let mut h = EditorHook::new(path.to_string_lossy().into_owned(), entries);
h.refresh_worlds();
h
}
pub(super) fn world_with_name_field() -> World {
let mut world = World::new();
for id in modal::all_field_ids() {
world.add_component(TextInput {
asset_id: id,
..Default::default()
});
}
world
}
pub(super) fn set_name(world: &mut World, text: &str) {
widget::seed_field(world, modal::NAME_INPUT, text);
}
pub(super) fn row_index(h: &EditorHook, name: &str) -> usize {
h.worlds_rows
.iter()
.position(|r| r.name == name)
.unwrap_or_else(|| panic!("{name} is not listed"))
}
pub(super) fn names(h: &EditorHook) -> Vec<String> {
h.worlds_rows.iter().map(|r| r.name.clone()).collect()
}
pub(super) fn button_index(h: &EditorHook, label: &str) -> usize {
let buttons = &h.modal.as_ref().expect("a dialog is open").buttons;
buttons
.iter()
.position(|b| b.label == label)
.unwrap_or_else(|| panic!("no '{label}' button"))
}
pub(super) fn press_modal(h: &mut EditorHook, world: &mut World, label: &str) {
let i = button_index(h, label);
let state = h.modal.as_ref().unwrap();
let (count, field) = (state.buttons.len(), state.field);
let r = modal::button_rect(modal::panel_rect(VP, field), count, i);
let input = FrameInput {
left_click: true,
mouse_x: r[0] + 2.0,
mouse_y: r[1] + 2.0,
viewport: VP,
..Default::default()
};
assert!(h.route_modal_click(&input, VP, world));
}
#[test]
fn the_listing_is_newest_first_and_marks_the_open_world() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[], 1_000);
write_world(&worlds_dir, "lobby", &[], 3_000);
write_world(dir.path(), "world", &[], 2_000);
let h = hook_at(&arena, Vec::new());
assert_eq!(names(&h), ["lobby", "world", "arena"]);
assert!(h.worlds_rows[row_index(&h, "arena")].open);
assert!(!h.worlds_rows[row_index(&h, "lobby")].open);
crate::test_support::isolate_state_dir();
}
#[test]
fn opening_a_world_retargets_the_whole_session() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
write_world(&worlds_dir, "lobby", &[entry("desk"), entry("lamp")], 2_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.worlds_open = true;
h.entries.push(entry("crate_b"));
h.mark_changed();
h.saved = h.entries.clone();
h.dirty = false;
h.selection.replace("crate_a".to_string());
h.hidden_assets.insert("crate_b".to_string());
h.rebuild_preview = false;
assert!(h.can_undo());
let mut world = world_with_name_field();
let i = row_index(&h, "lobby");
h.apply_worlds_action(WorldsAction::Open(i), &mut world);
assert_eq!(
h.world_path,
worlds_dir.join("lobby.jsonl").to_string_lossy()
);
assert_eq!(h.entries.len(), 2);
assert_eq!(h.entries[0]["name"], "desk");
assert_eq!(h.saved, h.entries, "the loaded list is what is on disk");
assert_eq!(h.baseline, h.entries);
assert!(!h.dirty);
assert!(
!h.can_undo(),
"the history belonged to the world left behind"
);
assert!(
h.rebuild_preview && h.rebuild_required,
"the compiled world is swapped on the next frame"
);
assert!(h.world_shadows.is_none());
assert!(h.tree_stale && h.tree_groups.is_empty());
assert_eq!(h.selection.iter().count(), 0);
assert!(h.hidden_assets.is_empty());
assert!(!h.worlds_open, "the panel has done its job");
assert!(h.worlds_rows[row_index(&h, "lobby")].open);
crate::test_support::isolate_state_dir();
}
#[test]
fn opening_an_unparseable_world_keeps_the_open_one() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
write_world(&worlds_dir, "broken", &[], 2_000);
std::fs::write(worlds_dir.join("broken.jsonl"), "{not json").unwrap();
let mut h = hook_at(&arena, vec![entry("crate_a")]);
let mut world = world_with_name_field();
let i = row_index(&h, "broken");
h.apply_worlds_action(WorldsAction::Open(i), &mut world);
assert_eq!(h.world_path, arena.to_string_lossy());
assert_eq!(h.entries.len(), 1);
assert!(h.worlds_status.is_some(), "the failure is reported");
crate::test_support::isolate_state_dir();
}
#[test]
fn plus_opens_an_untitled_world_that_the_first_save_names() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.worlds_open = true;
let mut world = world_with_name_field();
h.apply_worlds_action(WorldsAction::New, &mut world);
assert!(h.untitled, "the session is on a world with no home yet");
assert!(h.entries.is_empty() && !h.dirty);
assert!(!h.worlds_open, "the panel steps aside for the empty world");
assert!(h.modal.is_none(), "nothing is asked until a save");
assert_eq!(names(&h), ["arena"], "and nothing is on disk");
h.save();
assert!(h.naming_world(), "the prompt is up with its field");
set_name(&mut world, " lobby ");
press_modal(&mut h, &mut world, "Save");
let created = worlds_dir.join("lobby.jsonl");
assert!(created.exists());
assert_eq!(h.world_path, created.to_string_lossy());
assert!(!h.untitled && !h.dirty);
assert!(names(&h).contains(&"lobby".to_string()));
crate::test_support::isolate_state_dir();
}
#[test]
fn the_name_prompt_reopens_on_a_name_it_cannot_use() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[], 1_000);
let mut h = hook_at(&arena, Vec::new());
let mut world = world_with_name_field();
h.apply_worlds_action(WorldsAction::New, &mut world);
for (typed, expect) in [(" ", "name"), ("arena", "exists"), ("a/b", "/")] {
h.save();
set_name(&mut world, typed);
press_modal(&mut h, &mut world, "Save");
let message = h
.modal
.as_ref()
.map(|m| m.message.clone())
.unwrap_or_default();
assert!(
h.naming_world() && message.contains(expect),
"'{typed}' was rejected as: {message}"
);
assert!(h.untitled, "'{typed}' left the session untitled");
assert_eq!(names(&h), ["arena"], "nothing was created");
h.modal = None;
}
crate::test_support::isolate_state_dir();
}
#[test]
fn delete_asks_first_then_removes_the_file_and_its_session_entry() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[], 1_000);
let lobby = write_world(&worlds_dir, "lobby", &[], 2_000);
let store_path = session_store::default_path().expect("the temp project has a store");
let mut store = session_store::SessionStore::default();
store
.worlds
.insert("lobby".to_string(), session_store::WorldSession::default());
store
.worlds
.insert("arena".to_string(), session_store::WorldSession::default());
session_store::save(&store_path, &store).unwrap();
let mut h = hook_at(&arena, Vec::new());
let mut world = world_with_name_field();
let i = row_index(&h, "lobby");
h.apply_worlds_action(WorldsAction::Delete(i), &mut world);
let buttons = &h.modal.as_ref().expect("a dialog is open").buttons;
assert_eq!(buttons.len(), 2);
assert!(
buttons[button_index(&h, "Delete")].danger,
"the destructive button is marked"
);
press_modal(&mut h, &mut world, "Cancel");
assert!(h.modal.is_none() && lobby.exists());
h.apply_worlds_action(WorldsAction::Delete(i), &mut world);
press_modal(&mut h, &mut world, "Delete");
assert!(h.modal.is_none());
assert!(!lobby.exists());
assert_eq!(names(&h), ["arena"]);
let back = session_store::load(&store_path);
assert!(!back.worlds.contains_key("lobby"));
assert!(
back.worlds.contains_key("arena"),
"only the deleted one goes"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn deleting_the_open_world_keeps_the_session_and_marks_it_unsaved() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
let mut world = world_with_name_field();
let i = row_index(&h, "arena");
h.apply_worlds_action(WorldsAction::Delete(i), &mut world);
press_modal(&mut h, &mut world, "Delete");
assert!(!arena.exists());
assert_eq!(
h.world_path,
arena.to_string_lossy(),
"still the edit target"
);
assert_eq!(h.entries.len(), 1, "the session keeps its entries");
assert!(h.dirty, "nothing of the world is on disk any more");
assert!(h.worlds_rows.is_empty());
h.save();
assert!(arena.exists() && !h.dirty);
crate::test_support::isolate_state_dir();
}
#[test]
fn a_dirty_switch_asks_and_cancel_stays_put() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
write_world(&worlds_dir, "lobby", &[entry("desk")], 2_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.entries.push(entry("crate_b"));
h.mark_changed();
assert!(h.dirty);
let mut world = world_with_name_field();
let i = row_index(&h, "lobby");
h.apply_worlds_action(WorldsAction::Open(i), &mut world);
assert_eq!(h.modal.as_ref().unwrap().buttons.len(), 3);
assert!(h.modal.as_ref().unwrap().buttons[button_index(&h, "Discard")].danger);
press_modal(&mut h, &mut world, "Cancel");
assert_eq!(h.world_path, arena.to_string_lossy(), "nothing switched");
assert!(h.dirty, "the edits are still only in memory");
assert_eq!(h.entries.len(), 2);
crate::test_support::isolate_state_dir();
}
#[test]
fn a_dirty_switch_saves_before_switching() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
let lobby = write_world(&worlds_dir, "lobby", &[entry("desk")], 2_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.entries.push(entry("crate_b"));
h.mark_changed();
let mut world = world_with_name_field();
let i = row_index(&h, "lobby");
h.apply_worlds_action(WorldsAction::Open(i), &mut world);
press_modal(&mut h, &mut world, "Save");
let written = crate::world::parse_world_jsonl(&std::fs::read_to_string(&arena).unwrap())
.expect("the edits were written before the switch");
assert_eq!(written.len(), 2);
assert_eq!(h.world_path, lobby.to_string_lossy());
assert_eq!(h.entries.len(), 1, "and the switch happened");
assert!(!h.dirty);
crate::test_support::isolate_state_dir();
}
#[test]
fn a_dirty_switch_can_discard_the_edits() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
let lobby = write_world(&worlds_dir, "lobby", &[entry("desk")], 2_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.entries.push(entry("crate_b"));
h.mark_changed();
let mut world = world_with_name_field();
let i = row_index(&h, "lobby");
h.apply_worlds_action(WorldsAction::Open(i), &mut world);
press_modal(&mut h, &mut world, "Discard");
let untouched = crate::world::parse_world_jsonl(&std::fs::read_to_string(&arena).unwrap())
.expect("the world left behind still parses");
assert_eq!(untouched.len(), 1, "the edits were dropped, not written");
assert_eq!(h.world_path, lobby.to_string_lossy());
assert!(!h.dirty);
crate::test_support::isolate_state_dir();
}
#[test]
fn the_row_menu_opens_on_the_dot_and_closes_on_anything_else() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[], 1_000);
write_world(&worlds_dir, "lobby", &[], 3_000);
let mut h = hook_at(&arena, Vec::new());
h.worlds_open = true;
let mut world = world_with_name_field();
let i = row_index(&h, "arena");
h.apply_worlds_action(WorldsAction::OpenMenu(i), &mut world);
assert_eq!(
h.worlds_menu.as_deref(),
Some(arena.to_string_lossy().as_ref())
);
assert_eq!(h.make_worlds_view([0.0, 0.0]).menu, Some(i));
h.apply_worlds_action(WorldsAction::CloseMenu, &mut world);
assert!(h.worlds_menu.is_none());
h.apply_worlds_action(WorldsAction::OpenMenu(i), &mut world);
h.apply_worlds_action(WorldsAction::Delete(i), &mut world);
assert!(
h.worlds_menu.is_none(),
"the menu is done once it has picked"
);
assert!(h.modal.is_some(), "and the delete still asks first");
crate::test_support::isolate_state_dir();
}
#[test]
fn a_dirty_plus_asks_before_leaving_the_world() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
open_project(dir.path());
let worlds_dir = dir.path().join("worlds");
let arena = write_world(&worlds_dir, "arena", &[entry("crate_a")], 1_000);
let mut h = hook_at(&arena, vec![entry("crate_a")]);
h.entries.push(entry("crate_b"));
h.mark_changed();
let mut world = world_with_name_field();
h.apply_worlds_action(WorldsAction::New, &mut world);
assert_eq!(h.modal.as_ref().unwrap().buttons.len(), 3);
assert!(!h.untitled, "the world is not left until the guard clears");
press_modal(&mut h, &mut world, "Discard");
assert!(h.untitled && h.entries.is_empty());
crate::test_support::isolate_state_dir();
}
#[test]
fn panel_presses_are_rect_guarded() {
let mut h = EditorHook::new("unused.jsonl".to_string(), Vec::new());
h.worlds_open = true;
h.worlds_rows = vec![WorldRow {
name: "arena".to_string(),
path: "/p/worlds/arena.jsonl".to_string(),
open: false,
}];
let mut world = world_with_name_field();
let o = h.origin(PanelKey::Worlds, VP);
let s = registry::panel(PanelKey::Worlds).size(&h);
for (x, y) in [
(o[0] - 4.0, o[1] + 40.0),
(o[0] + s[0] + 4.0, o[1] + 40.0),
(o[0] + 40.0, o[1] + s[1] + 4.0),
] {
assert!(
!h.try_panel_press(PanelKey::Worlds, x, y, VP, &mut world),
"({x}, {y}) is off the panel"
);
}
assert!(h.try_panel_press(
PanelKey::Worlds,
o[0] + 40.0,
o[1] + s[1] - 4.0,
VP,
&mut world
));
h.worlds_open = false;
let r = worlds::Layout::new(worlds::Mode::Session, VP, 0.0).row_rect(o, 0);
assert!(!h.try_panel_press(PanelKey::Worlds, r[0] + 4.0, r[1] + 4.0, VP, &mut world));
}
#[test]
fn the_name_field_is_carried_across_a_preview_swap() {
let mut world = world_with_name_field();
widget::seed_field(&mut world, modal::NAME_INPUT, "half-typed");
let snapshot = EditorHook::field_snapshot(&world);
let mut fresh = world_with_name_field();
EditorHook::restore_fields(&mut fresh, &snapshot);
assert_eq!(widget::field_text(&fresh, modal::NAME_INPUT), "half-typed");
}