Files
gridcraft/crates/ui-egui/tests/it/error_checking_menu.rs
T
skylar taylor-barrick 41769f1d16 Migrate GridCraft to egui 0.37 (#2543)
* E37: migrate GridCraft to egui 0.37

Pin the reviewed fork and shared dependencies, adapt input and rendering to the
new frame lifecycle, and preserve bounded capture handling and font notices.
Add representative accessibility checks and meaningful control labels.

Validation: full repository CI passed all ten stages, with 2,249 tests passed,
zero failed and one existing timing test ignored. Native workflows and paired
performance qualification remain documented separately.

* egui037: record final published pins and local qualification

* M0.37: fix RTL clipping and preserve screenshot budgets

* test: share migrated GridCraft UI harness setup

* egui037: document migrated UI and regression contracts

* egui-037: align popup test fonts and avoid redundant tooltip probes
2026-10-25 14:55:00 -04:00

163 lines
6.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Formulas › Error Checking is a split button, as in Excel: the button opens the Error Checking
//! dialog, and its arrow opens a menu whose Circular References submenu lists a cell on each
//! circular reference and selects the one picked, on its own sheet.
use egui::{Event, Shape, pos2, vec2};
use egui_kittest::kittest::{NodeT as _, Queryable as _};
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
/// The submenu item's accessible name. egui 0.37 draws the submenu arrow as decoration and no
/// longer folds it into the label, so screen readers (and kittest) see the purpose without it.
const CIRCULAR: &str = "Circular References";
/// Builds the Formulas-tab harness, optionally adding cross-sheet and same-sheet circular references and selecting H20.
fn harness(loops: bool) -> Harness {
let mut s = Session::new();
s.new_workbook();
s.execute("home.insertSheet", json!({"name": "Q1 Data"})).unwrap();
if loops {
s.execute("cell.set", json!({"cell": "B3", "input": "=B4"})).unwrap();
s.execute("cell.set", json!({"cell": "B4", "input": "=B3"})).unwrap();
}
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
if loops {
s.execute("cell.set", json!({"cell": "C2", "input": "=C2+1"})).unwrap();
}
s.execute("selection.set", json!({"range": "H20"})).unwrap();
let mut h = crate::common::builder().with_size(vec2(1400.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
app.logic(ui.ctx());
app.ui(ui);
},
crate::common::app(s),
);
h.run_steps(4);
h.state_mut().ui.ribbon_tab = "Formulas".into();
h.run_steps(4);
h
}
/// The painted "Error Checking" ribbon label: its left edge, right edge and vertical center.
fn label(h: &Harness) -> (f32, f32, f32) {
h.output()
.shapes
.iter()
.find_map(|s| match &s.shape {
Shape::Text(t) if t.galley.job.text == "Error Checking" => {
Some((t.pos.x, t.pos.x + t.galley.size().x, t.pos.y + t.galley.size().y / 2.0))
}
_ => None,
})
.expect("the Formulas tab shows Error Checking")
}
fn click(h: &mut Harness, at: egui::Pos2) {
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(2);
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
h.run_steps(2);
}
h.run_steps(2);
}
fn open_menu(h: &mut Harness) {
let (_, right, y) = label(h);
// The arrow sits right of the label, past the 6 px gap after the text.
click(h, pos2(right + 13.0, y));
}
fn hover(h: &mut Harness, label: &str) {
let at = h.get_by_label(label).rect().center();
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(6);
}
#[test]
fn error_checking_button_opens_the_dialog_and_its_arrow_opens_the_menu() {
let mut h = harness(false);
let (left, _, y) = label(&h);
click(&mut h, pos2(left + 4.0, y));
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the button opens Error Checking");
h.state_mut().dialog = None;
h.run_steps(2);
open_menu(&mut h);
assert!(h.state().dialog.is_none(), "the arrow opens a menu, not the dialog");
h.get_by_label("Error Checking…").click();
h.run_steps(4);
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the menu's first item opens Error Checking");
}
#[test]
fn circular_references_is_unavailable_without_a_circular_reference() {
let mut h = harness(false);
open_menu(&mut h);
assert!(h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
}
#[test]
fn circular_references_lists_the_loops_and_selects_the_cell_picked() {
let mut h = harness(true);
open_menu(&mut h);
assert!(!h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
hover(&mut h, CIRCULAR);
assert!(h.query_by_label("Sheet1!C2").is_some(), "the loop on the active sheet is listed");
// A sheet name with a space is quoted, as in a formula.
let other = ["'Q1 Data'!B3", "'Q1 Data'!B4"].into_iter().find(|l| h.query_by_label(l).is_some()).expect("the loop on Q1 Data is listed");
h.get_by_label(other).click();
h.run_steps(4);
let app = h.state();
let d = app.session.active().unwrap();
assert_eq!(d.wb.active_sheet, 1, "picking a cell on Q1 Data goes to Q1 Data");
assert_eq!(format!("'Q1 Data'!{}", d.selection.active.a1()), other);
}
#[test]
fn circular_references_scrolls_to_the_cell_picked() {
let mut h = harness(false);
h.state_mut().run("cell.set", json!({"cell": "C500", "input": "=C500+1"})).unwrap();
h.state_mut().run("selection.set", json!({"range": "H20"})).unwrap();
h.run_steps(4);
assert_eq!(h.state().view().scroll.y, 0.0, "setup: the view is at the top of the sheet");
open_menu(&mut h);
hover(&mut h, CIRCULAR);
h.get_by_label("Sheet1!C500").click();
h.run_steps(4);
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "C500");
assert!(h.state().view().scroll.y > 0.0, "the grid scrolls to the cell picked");
}
#[test]
/// Wheel scrolling brings A100 into view, and clicking it selects A100.
fn circular_references_scrolls_a_list_taller_than_the_window() {
let mut h = harness(false);
for row in 1..=100 {
h.state_mut().run("cell.set", json!({"cell": format!("A{row}"), "input": format!("=A{row}+1")})).unwrap();
}
h.state_mut().run("selection.set", json!({"range": "H20"})).unwrap();
h.run_steps(4);
open_menu(&mut h);
hover(&mut h, CIRCULAR);
let window = h.ctx.content_rect();
assert!(h.get_by_label("Sheet1!A100").rect().bottom() > window.bottom(), "setup: the last cell starts below the window");
hover(&mut h, "Sheet1!A1");
for _ in 0..20 {
h.input_mut().events.push(Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
source: egui::MouseWheelSource::Unknown,
delta: vec2(0.0, -10.0),
phase: egui::TouchPhase::Move,
modifiers: egui::Modifiers::NONE,
});
h.run_steps(4);
}
let last = h.get_by_label("Sheet1!A100").rect();
assert!(last.bottom() <= window.bottom(), "the wheel brings the last cell into the window: {last:?} in {window:?}");
click(&mut h, last.center());
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "A100");
}