use concinnity_core::components::{FrameInput, InputKey, KeyEvent, KeyMods, KeyPress};
use concinnity_core::ecs::World;
use concinnity_core::window::clipboard::Clipboard;
use crate::editor::hook::EditorHook;
use crate::editor::hook::tests::fixtures::hook;
use crate::editor::inject;
use crate::editor::panels::registry::PanelKey;
use crate::editor::panels::story;
use crate::editor::panels::story_panel::{self, StoryAction};
use crate::editor::text_area::TextArea;
fn story_import(source: &str) -> serde_json::Value {
serde_json::json!({"type": "StoryImport", "args": {"$id": "tale", "source": source}})
}
fn story_session(text: &str) -> (EditorHook, World) {
story_session_at("unused.md", text)
}
fn story_session_at(source: &str, text: &str) -> (EditorHook, World) {
let mut world = World::new();
inject::editor_hud(&mut world);
let mut h = hook(vec![story_import(source)]);
h.viewport = [1280.0, 720.0];
h.open[PanelKey::Story] = true;
h.story.area = TextArea::from_text(text);
h.story.focus = true;
h.focus_panel(PanelKey::Story);
(h, world)
}
fn events(events: Vec<KeyEvent>) -> FrameInput {
FrameInput {
key_events: events,
viewport: [1280.0, 720.0],
..Default::default()
}
}
fn chord(key: InputKey) -> KeyEvent {
let mods = if cfg!(target_os = "macos") {
KeyMods::CMD
} else {
KeyMods::CTRL
};
KeyEvent::Press(KeyPress::new(key, mods))
}
#[test]
fn a_frame_of_keys_edits_the_focused_story() {
let (mut h, mut world) = story_session(" one");
h.story.area.go_to(0, 7);
let input = events(vec![
KeyEvent::press(InputKey::Enter),
KeyEvent::Text('t'),
KeyEvent::Text('w'),
KeyEvent::Text('o'),
]);
h.story_keys(&mut world, &input);
assert_eq!(h.story.area.text(), " one\n two");
assert!(h.make_story_view([0.0, 0.0]).area.is_dirty());
}
#[test]
fn keys_need_focus_and_the_front_panel() {
let (mut h, mut world) = story_session("x");
let typed = events(vec![KeyEvent::Text('y')]);
h.story.focus = false;
h.story_keys(&mut world, &typed);
assert_eq!(h.story.area.text(), "x", "an unfocused area ignores typing");
h.story.focus = true;
h.focus_panel(PanelKey::Console);
h.story_keys(&mut world, &typed);
assert_eq!(
h.story.area.text(),
"x",
"a panel in front takes the keyboard"
);
assert!(!h.make_story_view([0.0, 0.0]).focus);
h.focus_panel(PanelKey::Story);
h.story_keys(&mut world, &typed);
assert_eq!(h.story.area.text(), "yx");
}
#[test]
fn presses_focus_and_blur_the_area() {
let (mut h, _world) = story_session("first\nsecond");
h.story.focus = false;
let o = h.origin(PanelKey::Story, h.viewport);
let s = h.effective_size(PanelKey::Story);
let r = story_panel::area_rect(o, s);
let second_row = r[1] + 1.5 * crate::editor::code_font::LINE_H;
h.apply_story_action(StoryAction::Text, r[0] + r[2] - 20.0, second_row);
assert!(h.story.focus);
assert_eq!(h.story.area.caret().line, 1);
h.apply_story_action(StoryAction::Consume, 0.0, 0.0);
assert!(!h.story.focus);
}
#[test]
fn copy_and_paste_fall_back_to_the_internal_clipboard() {
let (mut h, mut world) = story_session("abc");
let input = events(vec![chord(InputKey::A), chord(InputKey::C)]);
h.story_keys(&mut world, &input);
assert_eq!(h.text_clipboard.text().as_deref(), Some("abc"));
let input = events(vec![KeyEvent::press(InputKey::End), chord(InputKey::V)]);
h.story_keys(&mut world, &input);
assert_eq!(h.story.area.text(), "abcabc");
}
#[test]
fn story_apply_validates_then_writes() {
let tree = concinnity_testing::TempTree::new();
let path = tree.write("tale.md", story::STARTER_STORY);
let src = path.to_string_lossy().to_string();
let mut h = hook(vec![story_import(&src)]);
h.open[PanelKey::Story] = true;
h.load_story();
assert_eq!(h.story.status, None);
assert_eq!(h.story.path, src);
assert_eq!(h.story.area.text(), story::STARTER_STORY);
h.story.area = TextArea::from_text("just prose, no frontmatter");
h.story.area.type_char('!');
h.apply_story();
assert!(h.story.status.is_some(), "parse failure shown");
assert!(
h.story.area.is_dirty(),
"a rejected apply leaves the edit unapplied"
);
assert!(!h.rebuild_preview);
let on_disk = std::fs::read_to_string(&path).unwrap();
assert_eq!(on_disk, story::STARTER_STORY, "file untouched");
let broken = story::STARTER_STORY.replace("The story begins here.", "[x](#nowhere) prose");
h.story.area = TextArea::from_text(&broken);
h.apply_story();
let marked: Vec<usize> = h.story.markers.iter().map(|m| m.line).collect();
let line = broken.lines().position(|l| l.contains("#nowhere")).unwrap();
assert_eq!(marked, [line]);
assert_eq!(
h.story.area.caret().line,
line,
"the caret jumps to the error"
);
h.load_story();
assert!(h.story.markers.is_empty(), "a reload clears the markers");
let last = h.story.area.line_count() - 1;
h.story.area.go_to(last, 0);
for c in "And they lived on.\n".chars() {
h.story.area.type_char(c);
}
h.apply_story();
assert_eq!(h.story.status, None);
assert!(!h.story.area.is_dirty(), "applied text is saved text");
assert!(h.rebuild_preview, "preview refresh requested");
assert!(!h.dirty, "no world.jsonl change");
let on_disk = std::fs::read_to_string(&path).unwrap();
assert!(on_disk.ends_with("The story begins here.\nAnd they lived on.\n"));
}
#[test]
fn the_save_shortcut_applies() {
let tree = concinnity_testing::TempTree::new();
let path = tree.write("tale.md", story::STARTER_STORY);
let (mut h, mut world) = story_session_at(&path.to_string_lossy(), "");
h.load_story();
h.story.focus = true;
h.story.area.go_to(0, 0);
let input = events(vec![KeyEvent::press(InputKey::Enter), chord(InputKey::S)]);
h.story_keys(&mut world, &input);
let on_disk = std::fs::read_to_string(&path).unwrap();
assert!(on_disk.starts_with("\n---"), "{on_disk:?}");
assert!(!h.story.area.is_dirty());
}
#[test]
fn story_load_missing_file_shows_status() {
let mut h = hook(vec![story_import("/no/such/dir/story.md")]);
h.open[PanelKey::Story] = true;
h.load_story();
assert!(h.story.status.is_some());
assert_eq!(h.story.area.text(), "");
}
#[test]
fn story_create_writes_starter_and_adds_the_import() {
let _guard = crate::test_support::lock();
let tree = concinnity_testing::TempTree::new();
let old = std::env::current_dir().unwrap();
std::env::set_current_dir(tree.path()).unwrap();
let mut h = hook(Vec::new());
h.open[PanelKey::Story] = true;
h.load_story();
assert!(
h.make_story_view([0.0, 0.0]).create,
"no import: create mode"
);
h.create_story();
assert_eq!(h.entries.len(), 1);
assert_eq!(h.entries[0]["type"], "StoryImport");
assert_eq!(h.entries[0]["args"]["source"], "story.md");
assert!(h.dirty, "the new entry is a world edit");
assert_eq!(h.story.area.text(), story::STARTER_STORY);
assert!(h.story.focus, "the new story is ready to type into");
assert!(!h.make_story_view([0.0, 0.0]).create);
let written = std::fs::read_to_string(tree.join("story.md")).unwrap();
assert_eq!(written, story::STARTER_STORY);
h.create_story();
assert_eq!(h.entries.len(), 1);
std::env::set_current_dir(old).unwrap();
}