Files
gridcraft/crates/ui-egui/tests/interaction.rs
T

202 lines
7.4 KiB
Rust

//! UI interaction tests: real egui input through the whole app (no GPU needed).
use egui::{Event, Key, Modifiers, PointerButton};
use gridcraft_engine::Session;
use gridcraft_engine::core::{CellRef, Value};
use gridcraft_ui_egui::{SheetApp, grid::Geo};
use serde_json::json;
fn harness(session: Session) -> egui_kittest::Harness<'static, SheetApp> {
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
SheetApp::new(session, Default::default()),
);
h.run_steps(4);
h
}
fn blank() -> Session {
let mut s = Session::new();
s.new_workbook();
s
}
fn key(h: &mut egui_kittest::Harness<'static, SheetApp>, k: Key, m: Modifiers) {
h.input_mut().events.push(Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: m });
h.input_mut().events.push(Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: m });
h.run_steps(2);
}
fn text(h: &mut egui_kittest::Harness<'static, SheetApp>, t: &str) {
h.input_mut().events.push(Event::Text(t.into()));
h.run_steps(2);
}
fn value(h: &egui_kittest::Harness<'static, SheetApp>, a: &str) -> Value {
h.state().session.active().and_then(|d| d.wb.active().map(|s| s.value(CellRef::parse(a).unwrap_or_default()))).unwrap_or_default()
}
fn click_cell(h: &mut egui_kittest::Harness<'static, SheetApp>, cell: &str) {
let app = h.state();
let sheet = app.session.active().unwrap().wb.active().unwrap();
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
let pos = geo.cell_rect(sheet, CellRef::parse(cell).unwrap()).center();
h.input_mut().events.extend([
Event::PointerMoved(pos),
Event::PointerButton { pos, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE },
]);
h.run_steps(2);
}
#[test]
fn keyboard_navigation_does_not_replay_the_last_pointer_click() {
let mut h = harness(blank());
click_cell(&mut h, "B2");
for (key_code, modifiers, expected) in [
(Key::Enter, Modifiers::NONE, "B3"),
(Key::Enter, Modifiers::NONE, "B4"),
(Key::Enter, Modifiers::NONE, "B5"),
(Key::Enter, Modifiers::SHIFT, "B4"),
(Key::Tab, Modifiers::NONE, "C4"),
(Key::Enter, Modifiers::NONE, "C5"),
] {
key(&mut h, key_code, modifiers);
let selection = &h.state().session.active().unwrap().selection;
assert_eq!(selection.active.a1(), expected);
assert_eq!(selection.anchor, selection.active);
assert!(h.state().editor.is_none(), "navigation must not start editing at the old pointer location");
}
// A subsequent real pointer click must still select the clicked cell.
click_cell(&mut h, "D6");
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "D6");
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "D7");
}
#[test]
fn space_toggles_the_active_checkbox_without_clicking_the_hovered_cell() {
let mut session = blank();
session.execute("insert.checkbox", json!({"range": "A1:B1"})).unwrap();
let mut h = harness(session);
click_cell(&mut h, "A1");
assert_eq!(value(&h, "A1"), Value::Bool(true));
key(&mut h, Key::ArrowRight, Modifiers::NONE);
key(&mut h, Key::Space, Modifiers::NONE);
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "B1");
assert_eq!(value(&h, "A1"), Value::Bool(true), "the old pointer location must remain unchanged");
assert_eq!(value(&h, "B1"), Value::Bool(true));
assert!(h.state().editor.is_none());
}
#[test]
fn typing_enters_values_and_moves_down() {
let mut h = harness(blank());
text(&mut h, "42");
assert!(h.state().editor.is_some(), "typing starts editing");
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(value(&h, "A1"), Value::Number(42.0));
text(&mut h, "=A1*2");
key(&mut h, Key::Tab, Modifiers::NONE);
assert_eq!(value(&h, "A2"), Value::Number(84.0));
let active = h.state().session.active().map(|d| d.selection.active.a1());
assert_eq!(active.as_deref(), Some("B2"));
}
#[test]
fn arrows_escape_and_undo() {
let mut h = harness(blank());
key(&mut h, Key::ArrowDown, Modifiers::NONE);
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert_eq!(h.state().session.active().map(|d| d.selection.active.a1()).as_deref(), Some("B2"));
text(&mut h, "hello");
key(&mut h, Key::Escape, Modifiers::NONE);
assert_eq!(value(&h, "B2"), Value::Empty);
text(&mut h, "x");
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(value(&h, "B2"), Value::from("x"));
key(&mut h, Key::Z, Modifiers::COMMAND);
assert_eq!(value(&h, "B2"), Value::Empty);
}
#[test]
fn shortcuts_format_and_extend() {
let mut s = blank();
s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2], [3, 4]]})).unwrap();
let mut h = harness(s);
key(&mut h, Key::ArrowRight, Modifiers::SHIFT);
key(&mut h, Key::ArrowDown, Modifiers::SHIFT);
assert_eq!(h.state().session.active().map(|d| d.selection.a1()).as_deref(), Some("A1:B2"));
key(&mut h, Key::B, Modifiers::COMMAND);
let bold = h.state_mut().session.run("cell.get", json!({"cell": "B2"})).unwrap()["style"]["font"]["bold"].clone();
assert_eq!(bold, json!(true));
key(&mut h, Key::Delete, Modifiers::NONE);
assert_eq!(value(&h, "A1"), Value::Empty);
}
#[test]
fn renders_every_ribbon_tab_without_panicking() {
let mut s = Session::new();
s.execute("file.new", json!({"sample": "sales"})).unwrap();
let mut h = harness(s);
for tab in ["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"] {
h.state_mut().ui.ribbon_tab = tab.into();
h.run_steps(2);
}
h.state_mut().ui.dark = true;
h.run_steps(3);
}
#[test]
fn dialogs_open_and_close() {
let mut h = harness(blank());
for name in [
"formatCells",
"insertFunction",
"find",
"sort",
"nameManager",
"dataValidation",
"pasteSpecial",
"commandSearch",
"spelling",
"goTo",
"about",
] {
h.state_mut().open_dialog(name, json!({}));
h.run_steps(2);
key(&mut h, Key::Escape, Modifiers::NONE);
h.state_mut().dialog = None;
h.state_mut().message = None;
}
}
#[test]
fn about_renders_every_tab() {
let mut h = harness(blank());
for tab in ["About", "Contributors", "Models"] {
h.state_mut().open_dialog("about", json!({"tab": tab}));
h.run_steps(2);
assert_eq!(h.state().dialog.as_ref().map(|d| d.tab.clone()).as_deref(), Some(tab));
h.state_mut().dialog = None;
}
}
#[test]
fn column_autocomplete_completes_on_enter() {
let mut s = blank();
s.execute("range.setValues", json!({"range": "A1", "values": [["North"], ["East"], ["Eastern"]]})).unwrap();
s.execute("selection.set", json!({"cell": "A4"})).unwrap();
let mut h = harness(s);
text(&mut h, "No");
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(value(&h, "A4"), Value::from("North"));
text(&mut h, "Ea"); // ambiguous: East / Eastern
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(value(&h, "A5"), Value::from("Ea"));
}