use concinnity_core::components::InputKey;
use concinnity_core::components::TextInput;
use concinnity_core::ecs::World;
use crate::editor::behavior;
use crate::editor::behavior::graph::CardKind;
use crate::editor::behavior::panel::{BehaviorAction, Status, ViewMode};
use crate::editor::behavior::path;
use crate::editor::hook::tests::fixtures::{
behavior, behavior_escape_input, behavior_row, behavior_session, entry, key_input, open_args,
press_behavior_key, press_remove, select_behavior, selected, type_name,
};
use crate::editor::hook::{EditorHook, declared_id};
use crate::editor::panels::registry::PanelKey;
use crate::editor::widget;
fn overview_card(h: &EditorHook, title: &str) -> usize {
h.behavior_data()
.overview
.cards
.iter()
.position(|c| c.title == title)
.unwrap_or_else(|| panic!("no `{title}` card on the overview"))
}
#[test]
fn behavior_panel_opens_on_the_first_behavior_and_steps_between_them() {
let (mut h, mut world) = behavior_session(vec![
entry("gfx", "GraphicsConfig"),
behavior("greet", serde_json::json!({"on": "start"})),
behavior("chase", serde_json::json!({"on": "tick"})),
]);
let data = h.behavior_data();
assert_eq!(
(data.name.as_str(), data.index, data.total),
("greet", 0, 2)
);
h.apply_behavior_action(BehaviorAction::Step(1), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior_data().name, "chase");
h.apply_behavior_action(BehaviorAction::Step(1), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior_data().name, "greet");
h.apply_behavior_action(BehaviorAction::Step(-1), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior_data().name, "chase");
}
#[test]
fn behavior_new_appends_a_blank_behavior_and_opens_it() {
let (mut h, mut world) = behavior_session(vec![entry("gfx", "GraphicsConfig")]);
assert_eq!(h.behavior_data().total, 0);
h.apply_behavior_action(BehaviorAction::New, &mut world, [0.0, 0.0]);
let data = h.behavior_data();
assert_eq!(data.total, 1);
assert_eq!(data.index, 0);
assert!(h.dirty && h.rebuild_preview, "adding one is a world edit");
assert_eq!(open_args(&h), serde_json::json!({"on": "start", "do": []}));
assert!(matches!(h.behavior.status, Some(Status::Ok)));
}
#[test]
fn behavior_status_reports_the_checkers_message() {
let (h, _) = behavior_session(vec![behavior(
"broken",
serde_json::json!({"do": [{"despawn": {"target": {"bind": "nope"}}}]}),
)]);
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected an error status, got {:?}", h.behavior.status);
};
assert!(e.contains("unbound name 'nope'"), "{e}");
assert!(e.starts_with("Behavior 'broken'"), "{e}");
}
#[test]
fn behavior_status_enforces_the_declared_variable_table() {
let vars = serde_json::json!({"type": "Variables",
"args": {"$id": "world_vars", "vars": [{"name": "health", "value": {"float": 100.0}}]}});
let (h, _) = behavior_session(vec![
vars,
behavior(
"heal",
serde_json::json!({"do": [{"set": {"var": "helth", "value": {"float": 1.0}}}]}),
),
]);
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected an error status, got {:?}", h.behavior.status);
};
assert!(e.contains("undeclared variable 'helth'"), "{e}");
}
#[test]
fn behavior_picking_a_node_appends_it_and_refreshes_the_preview() {
let (mut h, mut world) = behavior_session(vec![behavior("b", serde_json::json!({}))]);
select_behavior(&mut h, &mut world, "do");
h.apply_behavior_action(BehaviorAction::Palette, &mut world, [0.0, 0.0]);
assert!(h.behavior.picking);
let at = h
.behavior_data()
.picks
.iter()
.position(|p| p.verb == "hide")
.expect("hide is offered");
h.apply_behavior_action(BehaviorAction::Choose(at), &mut world, [0.0, 0.0]);
assert!(!h.behavior.picking, "picking closes the palette");
assert_eq!(
open_args(&h)["do"],
serde_json::json!([{"hide": {"target": "self"}}])
);
assert!(
h.rebuild_preview,
"the edited body runs in the live world straight away"
);
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected the scope error");
};
assert!(e.contains("`self` needs a `scope`"), "{e}");
}
#[test]
fn selecting_a_behavior_row_leaves_its_value_alone() {
let before = serde_json::json!({"on": "start", "once": false});
let (mut h, mut world) = behavior_session(vec![behavior("b", before.clone())]);
for label in ["on", "once"] {
select_behavior(&mut h, &mut world, label);
assert_eq!(open_args(&h), before, "selecting `{label}` edited it");
}
assert!(!h.rebuild_preview, "and nothing was committed");
}
#[test]
fn behavior_fixed_fields_are_set_from_the_palette() {
let (mut h, mut world) = behavior_session(vec![behavior(
"b",
serde_json::json!({"on": "start", "once": false}),
)]);
for (label, verb, want) in [
("on", "tick", serde_json::json!("tick")),
("once", "true", serde_json::json!(true)),
] {
select_behavior(&mut h, &mut world, label);
h.apply_behavior_action(BehaviorAction::Palette, &mut world, [0.0, 0.0]);
let at = h
.behavior_data()
.picks
.iter()
.position(|p| p.verb == verb)
.unwrap_or_else(|| panic!("`{label}` offers `{verb}`"));
h.apply_behavior_action(BehaviorAction::Choose(at), &mut world, [0.0, 0.0]);
assert_eq!(open_args(&h)[label], want);
}
}
#[test]
fn behavior_value_field_commits_on_enter_and_reports_a_bad_value() {
let (mut h, mut world) = behavior_session(vec![behavior(
"b",
serde_json::json!({"delay": 0.0, "do": []}),
)]);
select_behavior(&mut h, &mut world, "delay");
assert!(h.behavior.focus, "a typed row is ready to type into");
widget::seed_field(&mut world, behavior::panel::VALUE_INPUT, "2.5");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(open_args(&h)["delay"], serde_json::json!(2.5));
widget::seed_field(&mut world, behavior::panel::VALUE_INPUT, "soon");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(
open_args(&h)["delay"],
serde_json::json!(2.5),
"a rejected value leaves the old one standing"
);
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected a parse error");
};
assert!(e.contains("'soon' is not a number"), "{e}");
}
#[test]
fn behavior_delete_and_move_act_on_the_selected_member() {
let (mut h, mut world) = behavior_session(vec![behavior(
"b",
serde_json::json!({"scope": ["Prop"], "do": [
{"save": null}, {"hide": {"target": "self"}}]}),
)]);
select_behavior(&mut h, &mut world, "hide");
h.apply_behavior_action(BehaviorAction::Move(-1), &mut world, [0.0, 0.0]);
assert!(open_args(&h)["do"][0].get("hide").is_some());
assert_eq!(
h.behavior.row,
Some(behavior_row(&h, "hide")),
"the selection follows the node it moved"
);
h.apply_behavior_action(BehaviorAction::Delete, &mut world, [0.0, 0.0]);
assert_eq!(open_args(&h)["do"].as_array().unwrap().len(), 1);
assert!(
h.behavior.row.is_none(),
"the removed row's selection is dropped, not retargeted"
);
}
#[test]
fn behavior_remove_takes_the_open_behavior_not_a_node() {
let body = serde_json::json!({"scope": ["Prop"], "do": [{"hide": {"target": "self"}}]});
let (mut h, mut world) = behavior_session(vec![
behavior("greet", body.clone()),
behavior("chase", body.clone()),
]);
select_behavior(&mut h, &mut world, "hide");
press_remove(&mut h, &mut world);
press_remove(&mut h, &mut world);
assert_eq!(h.behavior_data().total, 1);
assert_eq!(h.behavior_data().name, "chase");
assert_eq!(open_args(&h), body, "the survivor's body is untouched");
assert!(h.dirty && h.rebuild_preview, "removing one is a world edit");
assert!(
h.entries.iter().all(|e| declared_id(e) != Some("greet")),
"the authored line is gone"
);
}
#[test]
fn behavior_remove_arms_first_and_any_other_press_cancels() {
let (mut h, mut world) = behavior_session(vec![behavior("greet", serde_json::json!({}))]);
press_remove(&mut h, &mut world);
assert!(h.behavior.remove_armed, "the first press only arms");
assert_eq!(h.behavior_data().total, 1, "and destroys nothing");
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
assert!(!h.behavior.remove_armed, "another press disarms it");
press_remove(&mut h, &mut world);
assert_eq!(
h.behavior_data().total,
1,
"so the next press arms again rather than committing"
);
press_remove(&mut h, &mut world);
assert_eq!(h.behavior_data().total, 0);
}
#[test]
fn behavior_remove_reopens_whatever_holds_that_ordinal() {
let (mut h, mut world) = behavior_session(vec![
behavior("a", serde_json::json!({})),
behavior("b", serde_json::json!({})),
behavior("c", serde_json::json!({})),
]);
h.apply_behavior_action(BehaviorAction::Step(2), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior_data().name, "c");
press_remove(&mut h, &mut world);
press_remove(&mut h, &mut world);
let data = h.behavior_data();
assert_eq!((data.name.as_str(), data.index, data.total), ("b", 1, 2));
for _ in 0..2 {
press_remove(&mut h, &mut world);
press_remove(&mut h, &mut world);
}
let data = h.behavior_data();
assert_eq!((data.name.as_str(), data.total), ("", 0));
assert!(h.behavior.status.is_none(), "and nothing to check");
}
#[test]
fn behavior_remove_is_undoable() {
let args = serde_json::json!({"on": "tick", "do": []});
let (mut h, mut world) = behavior_session(vec![behavior("greet", args.clone())]);
press_remove(&mut h, &mut world);
press_remove(&mut h, &mut world);
assert_eq!(h.behavior_data().total, 0);
h.undo(&mut world);
assert_eq!(h.behavior_data().total, 1);
assert_eq!(h.behavior_data().name, "greet");
assert_eq!(open_args(&h), args, "body and all");
}
#[test]
fn behavior_rename_commits_on_enter() {
let (mut h, mut world) = behavior_session(vec![
behavior("greet", serde_json::json!({})),
behavior("chase", serde_json::json!({})),
]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
assert!(h.behavior.name_focus);
assert_eq!(
widget::field_text(&world, behavior::panel::NAME_INPUT),
"greet",
"the field opens on the name it is about to replace"
);
type_name(&mut world, " welcome ");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(h.behavior_data().name, "welcome", "trimmed on the way in");
assert!(!h.behavior.name_focus, "committing gives up the keyboard");
assert!(h.dirty && h.rebuild_preview, "renaming is a world edit");
assert_eq!(h.behavior_data().index, 0);
}
#[test]
fn behavior_rename_keeps_the_name_unique() {
let (mut h, mut world) = behavior_session(vec![
behavior("greet", serde_json::json!({})),
behavior("chase", serde_json::json!({})),
]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
type_name(&mut world, "chase");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(h.behavior_data().name, "chase_1");
assert_eq!(
widget::field_text(&world, behavior::panel::NAME_INPUT),
"chase_1",
"the field shows what the world holds, not what was typed"
);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(h.behavior_data().name, "chase_1");
}
#[test]
fn behavior_rename_refuses_a_blank_name() {
let (mut h, mut world) = behavior_session(vec![behavior("greet", serde_json::json!({}))]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
type_name(&mut world, " ");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(h.behavior_data().name, "greet", "nothing was written");
assert!(!h.dirty, "and no edit was recorded");
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected the panel to say why, got {:?}", h.behavior.status);
};
assert!(e.contains("needs a name"), "{e}");
assert_eq!(
widget::field_text(&world, behavior::panel::NAME_INPUT),
"greet",
"the refused text is dropped rather than left to be committed later"
);
}
#[test]
fn behavior_rename_reruns_the_checker_under_the_new_name() {
let (mut h, mut world) = behavior_session(vec![behavior(
"broken",
serde_json::json!({"do": [{"despawn": {"target": {"bind": "nope"}}}]}),
)]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
type_name(&mut world, "still_broken");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
let Some(Status::Error { message: e, .. }) = &h.behavior.status else {
panic!("expected the error to survive the rename");
};
assert!(e.starts_with("Behavior 'still_broken'"), "{e}");
}
#[test]
fn behavior_name_reverts_when_it_loses_focus() {
let (mut h, mut world) = behavior_session(vec![behavior("greet", serde_json::json!({}))]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
type_name(&mut world, "half typed");
h.apply_behavior_action(BehaviorAction::Consume, &mut world, [0.0, 0.0]);
assert!(!h.behavior.name_focus);
assert_eq!(
widget::field_text(&world, behavior::panel::NAME_INPUT),
"greet"
);
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(h.behavior_data().name, "greet");
assert!(!h.dirty);
}
#[test]
fn behavior_name_and_value_fields_do_not_share_the_keyboard() {
let (mut h, mut world) = behavior_session(vec![behavior(
"b",
serde_json::json!({"delay": 0.0, "do": []}),
)]);
select_behavior(&mut h, &mut world, "delay");
assert!(h.behavior.focus && !h.behavior.name_focus);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
assert!(h.behavior.name_focus && !h.behavior.focus);
h.apply_behavior_action(BehaviorAction::FocusValue, &mut world, [0.0, 0.0]);
assert!(h.behavior.focus && !h.behavior.name_focus);
}
#[test]
fn behavior_value_field_is_carried_across_a_preview_rebuild() {
let (_, mut world) = behavior_session(vec![behavior("b", serde_json::json!({}))]);
widget::seed_field(&mut world, behavior::panel::VALUE_INPUT, "half typed");
let snapshot = EditorHook::field_snapshot(&world);
let mut fresh = World::new();
for id in behavior::panel::ids().field_ids() {
fresh.push_identified(id, TextInput::default());
}
EditorHook::restore_fields(&mut fresh, &snapshot);
assert_eq!(
widget::field_text(&fresh, behavior::panel::VALUE_INPUT),
"half typed"
);
}
#[test]
fn behavior_view_cycles_through_the_three_views() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": [{"hide": {"target": "self"}}]}),
)]);
assert_eq!(h.behavior.mode, ViewMode::Outline);
select_behavior(&mut h, &mut world, "hide");
let selected = h.behavior.row;
for want in [ViewMode::Chart, ViewMode::Overview, ViewMode::Outline] {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
assert_eq!(h.behavior.mode, want);
assert_eq!(
h.behavior.row, selected,
"the selection survives every switch"
);
}
}
#[test]
fn behavior_card_selects_the_row_it_stands_for() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": [
{"let": {"name": "t", "value": {"first": "q"}}},
{"hide": {"target": "self"}},
]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let card = h
.behavior_data()
.chart
.cards
.iter()
.position(|c| c.title == "hide")
.unwrap();
h.apply_behavior_action(BehaviorAction::SelectCard(card), &mut world, [0.0, 0.0]);
let rows = h.behavior_rows();
assert_eq!(rows[h.behavior.row.unwrap()].label, "hide");
assert!(
h.behavior_data()
.picks
.iter()
.any(|p| p.verb == "set_transform"),
);
}
#[test]
fn behavior_overview_opens_the_behavior_a_card_stands_for() {
let (mut h, mut world) = behavior_session(vec![
behavior(
"award",
serde_json::json!({"on": "start",
"do": [{"set": {"var": "score", "value": {"int": 1}}}]}),
),
behavior(
"react",
serde_json::json!({"on": {"variable": "score"}, "do": []}),
),
]);
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
assert_eq!(h.behavior.mode, ViewMode::Overview);
let overview = h.behavior_data().overview;
let card = overview
.cards
.iter()
.position(|c| c.title == "react")
.expect("react is on the map");
assert!(
overview.cards.iter().any(|c| c.title == "score"),
"the variable joining them is a card of its own"
);
h.apply_behavior_action(BehaviorAction::OpenCard(card), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior.index, 1);
assert_eq!(h.behavior_data().name, "react");
assert_eq!(
h.behavior.mode,
ViewMode::Chart,
"and lands on the body it named"
);
}
#[test]
fn an_overview_asset_card_selects_the_place_it_stands_for() {
let (mut h, mut world) = behavior_session(vec![
entry("hub", "Scene"),
behavior(
"finish",
serde_json::json!({"on": "start", "do": [{"scene": {"scene": "hub"}}]}),
),
]);
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
let card = overview_card(&h, "hub");
h.apply_behavior_action(BehaviorAction::SelectAsset(card), &mut world, [0.0, 0.0]);
assert_eq!(selected(&h), ["hub"]);
assert_eq!(h.behavior.overview_card, Some(card));
let places = h.map_chart();
assert_eq!(
h.make_map_view(&places, [-1.0, -1.0]).selected,
places.cards.iter().position(|c| c.title == "hub"),
);
}
#[test]
fn opening_an_overview_behavior_selects_that_behavior() {
let (mut h, mut world) = behavior_session(vec![
behavior("first", serde_json::json!({"on": "start"})),
behavior("second", serde_json::json!({"on": "tick"})),
]);
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
let card = overview_card(&h, "second");
h.apply_behavior_action(BehaviorAction::OpenCard(card), &mut world, [0.0, 0.0]);
assert_eq!(h.behavior_data().name, "second");
assert_eq!(selected(&h), ["second"]);
}
#[test]
fn an_overview_card_for_an_undeclared_name_selects_nothing() {
let (mut h, mut world) = behavior_session(vec![behavior(
"escape",
serde_json::json!({"on": "start", "do": [{"scene": {"scene": "typo"}}]}),
)]);
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
let card = overview_card(&h, "typo");
let overview = h.behavior_data().overview;
assert_eq!(overview.cards[card].kind, CardKind::Missing);
assert_eq!(overview.cards[card].handle, None);
h.apply_behavior_action(BehaviorAction::SelectAsset(card), &mut world, [0.0, 0.0]);
assert!(h.selection.is_empty(), "{:?}", selected(&h));
}
#[test]
fn behavior_overview_is_built_only_while_it_shows() {
let (mut h, mut world) = behavior_session(vec![behavior(
"a",
serde_json::json!({"on": "start", "do": []}),
)]);
assert!(h.behavior_data().overview.cards.is_empty());
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
assert!(!h.behavior_data().overview.cards.is_empty());
}
#[test]
fn behavior_chart_appends_to_a_body_that_already_has_nodes() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [{"save": {}}]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let tail = h
.behavior_data()
.chart
.cards
.iter()
.position(|c| c.kind == CardKind::Add && c.path == [path::field("do")])
.expect("the body's chain ends in a card that appends to it");
h.apply_behavior_action(BehaviorAction::SelectCard(tail), &mut world, [0.0, 0.0]);
let pick = h
.behavior_data()
.picks
.iter()
.position(|p| p.verb == "hide")
.expect("it offers the node palette");
h.apply_behavior_action(BehaviorAction::Choose(pick), &mut world, [0.0, 0.0]);
assert_eq!(
open_args(&h)["do"],
serde_json::json!([{"save": {}}, {"hide": {"target": "self"}}]),
"the node was appended after the one already there"
);
assert!(h.rebuild_preview, "and the live world has it");
}
#[test]
fn behavior_chart_reaches_the_settings_the_behavior_declares() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "scope": ["Prop"], "do": [{"save": {}}]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
h.apply_behavior_action(BehaviorAction::SelectCard(0), &mut world, [0.0, 0.0]);
let data = h.behavior_data();
let listed: Vec<&str> = data
.fields
.iter()
.map(|&i| data.rows[i].label.as_str())
.collect();
for want in ["on", "once", "delay", "cooldown", "scope"] {
assert!(listed.contains(&want), "{want} in {listed:?}");
}
let once = data.fields[listed.iter().position(|l| *l == "once").unwrap()];
h.apply_behavior_action(BehaviorAction::Select(once), &mut world, [0.0, 0.0]);
h.apply_behavior_action(BehaviorAction::Palette, &mut world, [0.0, 0.0]);
let pick = h
.behavior_data()
.picks
.iter()
.position(|p| p.verb == "true")
.expect("a flag offers its two options");
h.apply_behavior_action(BehaviorAction::Choose(pick), &mut world, [0.0, 0.0]);
assert_eq!(open_args(&h)["once"], serde_json::json!(true));
}
#[test]
fn behavior_inspector_holds_the_node_while_its_fields_are_selected() {
let (mut h, mut world) = behavior_session(vec![behavior(
"drip",
serde_json::json!({"on": "start", "do": [
{"spawn": {"template": "drop", "lifetime": 4.0}},
]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let card = h
.behavior_data()
.chart
.cards
.iter()
.position(|c| c.title == "spawn")
.unwrap();
h.apply_behavior_action(BehaviorAction::SelectCard(card), &mut world, [0.0, 0.0]);
let data = h.behavior_data();
assert_eq!(data.card, Some(card));
let listed: Vec<&str> = data
.fields
.iter()
.map(|&i| data.rows[i].label.as_str())
.collect();
assert!(listed.contains(&"lifetime"), "{listed:?}");
let lifetime = data.fields[listed.iter().position(|l| *l == "lifetime").unwrap()];
h.apply_behavior_action(BehaviorAction::Select(lifetime), &mut world, [0.0, 0.0]);
let after = h.behavior_data();
assert_eq!(after.card, Some(card), "the inspector holds the node");
assert_eq!(after.fields, data.fields, "and lists the same settings");
assert!(h.behavior.focus, "a typed setting is ready to type into");
}
#[test]
fn behavior_inspector_stops_at_the_nodes_a_branch_holds() {
let (mut h, mut world) = behavior_session(vec![behavior(
"gate",
serde_json::json!({"on": "start", "do": [
{"if": {"cond": {"bool": true}, "then": [{"save": null}], "else": []}},
]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let card = h
.behavior_data()
.chart
.cards
.iter()
.position(|c| c.title == "if")
.unwrap();
h.apply_behavior_action(BehaviorAction::SelectCard(card), &mut world, [0.0, 0.0]);
let data = h.behavior_data();
let listed: Vec<&str> = data
.fields
.iter()
.map(|&i| data.rows[i].label.as_str())
.collect();
assert!(listed.contains(&"cond"), "{listed:?}");
assert!(!listed.contains(&"save"), "{listed:?}");
}
#[test]
fn behavior_empty_branch_card_appends_into_that_branch() {
let (mut h, mut world) = behavior_session(vec![behavior(
"gate",
serde_json::json!({"on": "start", "do": [
{"if": {"cond": {"bool": true}, "then": [{"save": {}}]}},
]}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let card = h
.behavior_data()
.chart
.cards
.iter()
.position(|c| c.kind == CardKind::Add && c.path.last() == Some(&path::field("else")))
.unwrap();
h.apply_behavior_action(BehaviorAction::SelectCard(card), &mut world, [0.0, 0.0]);
let pick = h
.behavior_data()
.picks
.iter()
.position(|p| p.verb == "hide")
.unwrap();
h.apply_behavior_action(BehaviorAction::Choose(pick), &mut world, [0.0, 0.0]);
let body = open_args(&h);
assert_eq!(
body["do"][0]["if"]["else"][0]["hide"],
serde_json::json!({"target": "self"}),
"{body}",
);
}
#[test]
fn behavior_wheel_pans_the_chart_within_its_extent() {
let body: Vec<serde_json::Value> = (0..8).map(|_| serde_json::json!({"save": {}})).collect();
let (mut h, mut world) = behavior_session(vec![behavior(
"long",
serde_json::json!({"on": "start", "do": body}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
h.scroll_behavior(1.0);
assert!(h.behavior.pan[0] > 0.0, "{:?}", h.behavior.pan);
assert_eq!(h.behavior.scroll, 0, "the outline's scroll is untouched");
for _ in 0..200 {
h.scroll_behavior(1.0);
}
let chart = h.behavior_data().chart;
let canvas =
behavior::panel::chart_canvas(h.effective_size(PanelKey::Behavior), ViewMode::Chart);
assert_eq!(
h.behavior.pan,
crate::editor::behavior::chart::clamp_pan(h.behavior.pan, &chart, canvas)
);
}
#[test]
fn behavior_selection_pans_an_off_canvas_card_into_view() {
let body: Vec<serde_json::Value> = (0..12).map(|_| serde_json::json!({"save": {}})).collect();
let (mut h, mut world) = behavior_session(vec![behavior(
"long",
serde_json::json!({"on": "start", "do": body}),
)]);
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
let last = h
.behavior_data()
.chart
.cards
.iter()
.rposition(|c| c.kind == CardKind::Node)
.unwrap();
h.apply_behavior_action(BehaviorAction::SelectCard(last), &mut world, [0.0, 0.0]);
h.apply_behavior_action(BehaviorAction::Move(-1), &mut world, [0.0, 0.0]);
let data = h.behavior_data();
let path = &data.rows[h.behavior.row.unwrap()].path;
let card = data.chart.cards.iter().find(|c| &c.path == path).unwrap();
let band = behavior::panel::chart_band(
[0.0, 0.0],
h.effective_size(PanelKey::Behavior),
ViewMode::Chart,
);
let rect = crate::editor::behavior::chart::card_rect(card, band, h.behavior.pan);
assert!(rect[0] >= band[0], "{rect:?} left of {band:?}");
assert!(rect[0] + rect[2] <= band[0] + band[2] + 0.01, "{rect:?}");
}
#[test]
fn behavior_status_points_at_the_row_the_checker_named() {
let (h, _world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [
{"save": {}},
{"hide": {"target": {"int": 1}}},
]}),
)]);
let data = h.behavior_data();
let view = h.make_behavior_view(&data, [0.0, 0.0]);
assert!(
view.status
.and_then(behavior::panel::Status::error)
.is_some(),
"an entity field holding an int does not check out"
);
let row = view.fault_row.expect("the checker located it");
assert_eq!(data.rows[row].label, "target");
assert_eq!(
data.rows[row].path,
vec![
crate::editor::behavior::path::field("do"),
crate::editor::behavior::path::Step::Index(1),
crate::editor::behavior::path::field("hide"),
crate::editor::behavior::path::field("target"),
],
);
}
#[test]
fn behavior_status_with_nothing_to_blame_points_nowhere() {
let (h, _world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [{"save": {}}]}),
)]);
let data = h.behavior_data();
assert!(h.make_behavior_view(&data, [0.0, 0.0]).status == Some(&behavior::panel::Status::Ok));
let (h, _world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "spawned", "do": []}),
)]);
let data = h.behavior_data();
let view = h.make_behavior_view(&data, [0.0, 0.0]);
assert!(
view.status
.and_then(behavior::panel::Status::error)
.is_some()
);
let row = view.fault_row.expect("the source row");
assert_eq!(data.rows[row].label, "on");
}
#[test]
fn behavior_go_to_fault_selects_the_faulting_row() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [
{"save": {}},
{"hide": {"target": {"int": 1}}},
]}),
)]);
assert_eq!(h.behavior.row, None);
h.apply_behavior_action(BehaviorAction::GoToFault, &mut world, [0.0, 0.0]);
let row = h.behavior.row.expect("the fault was selected");
assert_eq!(h.behavior_rows()[row].label, "target");
}
#[test]
fn behavior_go_to_fault_leaves_the_overview_for_the_body() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [{"hide": {"target": {"int": 1}}}]}),
)]);
for _ in 0..2 {
h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
}
assert_eq!(h.behavior.mode, ViewMode::Overview);
h.apply_behavior_action(BehaviorAction::GoToFault, &mut world, [0.0, 0.0]);
assert_eq!(h.behavior.mode, ViewMode::Chart);
let row = h.behavior.row.expect("the fault was selected");
assert_eq!(h.behavior_rows()[row].label, "target");
}
#[test]
fn a_stale_behavior_fault_never_points_off_its_own_path() {
let (mut h, _world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "start", "do": [{"hide": {"target": {"int": 1}}}]}),
)]);
let located = {
let data = h.behavior_data();
let row = h.make_behavior_view(&data, [0.0, 0.0]).fault_row.unwrap();
data.rows[row].path.clone()
};
let mut args = open_args(&h);
args["do"]
.as_array_mut()
.unwrap()
.insert(0, serde_json::json!({"save": {}}));
let idx = h.behavior_entry().unwrap();
h.entries[idx]
.as_object_mut()
.unwrap()
.insert("args".to_string(), args);
let data = h.behavior_data();
let row = h.make_behavior_view(&data, [0.0, 0.0]).fault_row;
let path = row.map(|i| data.rows[i].path.clone()).unwrap_or_default();
assert!(
crate::editor::behavior::path::starts_with(&located, &path),
"pointed at {path:?}, which is not on the way to {located:?}",
);
assert_ne!(
path, located,
"the exact spot is gone, so it settled for less"
);
}
fn type_filter(h: &mut EditorHook, world: &mut World, text: &str) {
widget::seed_field(world, behavior::panel::FILTER_INPUT, text);
h.sample_behavior_filter(world);
}
fn open_palette(h: &mut EditorHook, world: &mut World, row: &str) {
select_behavior(h, world, row);
h.apply_behavior_action(BehaviorAction::Palette, world, [0.0, 0.0]);
assert!(h.behavior.picking, "the palette is open");
}
#[test]
fn behavior_palette_filter_narrows_and_enter_takes_the_best_match() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
open_palette(&mut h, &mut world, "do");
let unfiltered = h.behavior_data().matches.len();
type_filter(&mut h, &mut world, "foreach");
let data = h.behavior_data();
assert!(data.matches.len() < unfiltered, "the query narrowed it");
assert_eq!(data.picks[data.matches[0]].verb, "for_each");
press_behavior_key(&mut h, &mut world, InputKey::Enter);
assert!(
open_args(&h)["do"][0].get("for_each").is_some(),
"the best match is what landed: {:?}",
open_args(&h)["do"],
);
}
#[test]
fn behavior_palette_filter_clears_when_the_palette_closes() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
open_palette(&mut h, &mut world, "do");
let unfiltered = h.behavior_data().matches.len();
type_filter(&mut h, &mut world, "spawn");
assert!(h.behavior_data().matches.len() < unfiltered);
press_behavior_key(&mut h, &mut world, InputKey::Enter);
assert!(h.behavior.filter.is_empty());
assert_eq!(
widget::field_text(&world, behavior::panel::FILTER_INPUT),
"",
"the field was cleared too, not just the mirror"
);
open_palette(&mut h, &mut world, "do");
type_filter(&mut h, &mut world, "spawn");
h.behavior_keys(&mut world, &behavior_escape_input());
assert!(!h.behavior.picking);
assert!(h.behavior.filter.is_empty());
open_palette(&mut h, &mut world, "do");
assert_eq!(h.behavior_data().matches.len(), unfiltered);
}
#[test]
fn behavior_palette_filter_resets_the_highlight_it_may_have_excluded() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
open_palette(&mut h, &mut world, "do");
for _ in 0..4 {
press_behavior_key(&mut h, &mut world, InputKey::Down);
}
assert_eq!(h.behavior.pick, 4);
type_filter(&mut h, &mut world, "spawn");
assert_eq!(h.behavior.pick, 0);
assert_eq!(h.behavior.pick_scroll, 0);
assert!(h.behavior_data().matches.len() <= 4);
press_behavior_key(&mut h, &mut world, InputKey::Enter);
assert!(
open_args(&h)["do"][0].get("spawn").is_some(),
"Enter still picked: {:?}",
open_args(&h)["do"],
);
}
#[test]
fn behavior_palette_survives_a_query_nothing_answers() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
open_palette(&mut h, &mut world, "do");
type_filter(&mut h, &mut world, "zzzz");
assert!(h.behavior_data().matches.is_empty());
assert!(h.behavior.picking, "the palette is still up");
press_behavior_key(&mut h, &mut world, InputKey::Enter);
assert!(h.behavior.picking);
assert_eq!(open_args(&h)["do"].as_array().map(Vec::len), Some(0));
type_filter(&mut h, &mut world, "save");
assert!(!h.behavior_data().matches.is_empty());
}
#[test]
fn behavior_palette_filter_holds_the_keyboard_off_the_shortcuts() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
assert!(!h.text_focus_active());
open_palette(&mut h, &mut world, "do");
assert!(
h.text_focus_active(),
"typing `s` into the filter must not reach the scale gizmo"
);
}
#[test]
fn behavior_palette_filter_field_draws_above_the_backing_it_sits_in() {
let (mut h, mut world) = behavior_session(vec![behavior(
"chase",
serde_json::json!({"on": "tick", "do": []}),
)]);
open_palette(&mut h, &mut world, "do");
let layers = h.compute_layers();
let field = layers[&behavior::panel::FILTER_INPUT];
assert!(
field > layers[&behavior::panel::PANEL_BG],
"the field sank into its own panel"
);
assert_eq!(
field,
layers[&behavior::panel::DROP_BG],
"the field and the backing it sits in share a layer"
);
}
#[test]
fn behavior_rename_moves_what_names_it() {
let (mut h, mut world) = behavior_session(vec![
behavior("greet", serde_json::json!({})),
behavior(
"chase",
serde_json::json!({"do": [{"despawn": {"target": {"named": "greet"}}}]}),
),
]);
h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
type_name(&mut world, "welcome");
h.behavior_keys(&mut world, &key_input(InputKey::Enter));
assert_eq!(declared_id(&h.entries[0]), Some("welcome"));
let named = &h.entries[1]["args"]["do"][0]["despawn"]["target"]["named"];
assert_eq!(named, "welcome");
h.undo(&mut world);
assert_eq!(declared_id(&h.entries[0]), Some("greet"));
assert_eq!(
h.entries[1]["args"]["do"][0]["despawn"]["target"]["named"],
"greet"
);
assert!(!h.can_undo(), "one step");
}