mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 14:17:12 -06:00
202 lines
7.4 KiB
Rust
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"));
|
|
}
|