//! Headless UI tests: two frames at 900x700 with egui's default fonts, //! driving the editor through `RawInput` events. use notescraft_editor::colors::EditorColors; use notescraft_editor::completion; use notescraft_editor::editor::{EditorOptions, EditorState, set_byte_cursor_range, show_editor, show_modal_editor, widget_id}; use egui::{Event, Key, Modifiers, Pos2, RawInput, Rect, Vec2}; fn input(events: Vec) -> RawInput { RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(900.0, 700.0))), events, ..RawInput::default() } } /// Runs one frame; the editor gets focus first so it sees the events. fn frame(ctx: &egui::Context, opts: &EditorOptions, state: &mut EditorState, events: Vec) { let colors = EditorColors::fallback_dark(); let out = ctx.run_ui(input(events), |ui| { ui.memory_mut(|mem| mem.request_focus(ui.id().with(opts.id_salt))); let _ = show_editor(ui, state, &colors, &|t| !t.contains("missing"), opts); }); out.drop_without_applying_deltas(); } #[test] fn editor_renders_and_applies_typed_text() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::new(), cursor: None, version: 0, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![]); // first frame: just render assert_eq!(state.text, ""); frame(&ctx, &opts, &mut state, vec![Event::Text(String::from("hello"))]); assert_eq!(state.text, "hello", "TextEdit inserts the Text event"); assert_eq!(state.version, 1, "one bump for the one modifying frame"); assert_eq!(state.cursor, Some(5)); // Enter without list marker: ordinary newline kept by TextEdit. frame(&ctx, &opts, &mut state, vec![key(Key::Enter)]); assert_eq!(state.text, "hello\n"); // Cmd/Ctrl+Enter makes the caret line a task: no newline, no continuation. let mods = Modifiers { command: true, ..Modifiers::default() }; let cmd = Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: mods }; frame(&ctx, &opts, &mut state, vec![cmd]); assert_eq!(state.text, "hello\n- [ ] ", "Cmd+Enter inserts no newline; it turns the empty line into a task"); } fn key(key: Key) -> Event { Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() } } #[test] fn enter_continues_list_through_raw_input() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::from("- item"), cursor: Some(6), version: 1, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![]); // establish focus frame(&ctx, &opts, &mut state, vec![key(Key::Enter)]); assert_eq!(state.text, "- item\n- ", "list continuation consumed the Enter"); } #[test] fn enter_on_numbered_list_increments() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::from("2. one"), cursor: Some(6), version: 1, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![]); frame(&ctx, &opts, &mut state, vec![key(Key::Enter)]); assert_eq!(state.text, "2. one\n3. "); } #[test] fn auto_close_pair_and_skip_over() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::new(), cursor: None, version: 0, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![Event::Text(String::from("["))]); assert_eq!(state.text, "[]", "auto-closed"); frame(&ctx, &opts, &mut state, vec![Event::Text(String::from("]"))]); assert_eq!(state.text, "[]", "typed closer skips over, no double bracket"); } #[test] fn tab_inserts_four_spaces() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::new(), cursor: None, version: 0, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![key(Key::Tab)]); assert_eq!(state.text, " ", "Tab became four spaces"); } #[test] fn popup_renders_without_selection_click() { let ctx = egui::Context::default(); let items = vec![String::from("alpha"), String::from("Beta")]; let mut chosen = None; let colors = EditorColors::fallback_dark(); let out = ctx.run_ui(input(vec![]), |ui| { let opts = EditorOptions::default(); let mut state = EditorState::default(); ui.memory_mut(|mem| mem.request_focus(ui.id().with(opts.id_salt))); let resp = show_editor(ui, &mut state, &colors, &|t| !t.contains("missing"), &opts); let _ = resp; let anchor = Rect::from_min_size(Pos2::ZERO, Vec2::new(80.0, 20.0)); chosen = completion::show_popup(ui, anchor, &items, 0); }); out.drop_without_applying_deltas(); assert_eq!(chosen, None); } #[test] fn two_frames_stay_stable() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let mut state = EditorState { text: String::from("# Title\n\n- a\n- b\n"), cursor: None, version: 7, ..Default::default() }; frame(&ctx, &opts, &mut state, vec![]); let v1 = state.version; frame(&ctx, &opts, &mut state, vec![]); assert_eq!(state.version, v1, "no phantom edits on idle frames"); } #[test] fn modal_editor_paints_block_at_unicode_and_line_end_cursors() { let ctx = egui::Context::default(); let opts = EditorOptions::default(); let colors = EditorColors::fallback_dark(); for (text, cursor) in [("é🦀\n", 2), ("é🦀\n", 6), ("", 0)] { let mut state = EditorState { text: String::from(text), ..Default::default() }; let out = ctx.run_ui(input(vec![]), |ui| { let id = widget_id(ui, &opts); ui.memory_mut(|mem| mem.request_focus(id)); set_byte_cursor_range(ui.ctx(), id, text, (cursor, cursor)).expect("valid cursor"); let style_before = ui.visuals().text_cursor.clone(); let response = show_modal_editor(ui, &mut state, &colors, &|_| true, &opts, true); assert_eq!(ui.visuals().text_cursor, style_before); assert!(response.caret_rect.is_some()); }); let block_color = colors.accent.gamma_multiply(0.4); assert!( out.shapes.iter().any( |shape| matches!(&shape.shape, egui::Shape::Rect(rect) if rect.fill == block_color && rect.rect.width() > 0.0 && rect.rect.height() > 0.0) ), "missing block for {text:?} at {cursor}: {:?}", out.shapes .iter() .filter_map(|shape| match &shape.shape { egui::Shape::Rect(rect) => Some(rect), _ => None, }) .collect::>() ); assert_eq!(state.cursor, Some(cursor)); assert_eq!(state.version, 0); out.drop_without_applying_deltas(); } }