67bb5fe06f
Ticks or unticks a box, adds a box to a list item, makes a plain or empty line a task. Round 5 tasks 36-37 added; the Visual block editor's version (37) is open. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
158 lines
6.8 KiB
Rust
158 lines
6.8 KiB
Rust
//! 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<Event>) -> 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<Event>) {
|
|
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::<Vec<_>>()
|
|
);
|
|
assert_eq!(state.cursor, Some(cursor));
|
|
assert_eq!(state.version, 0);
|
|
out.drop_without_applying_deltas();
|
|
}
|
|
}
|