Manage Rules: test reaching rules by scrolling down and sideways

Scroll the 3,000-rule list to a row that was off screen and move and
delete that rule through its own buttons; give one rule a label of 200
ranges whose buttons start outside the dialog, scroll sideways, then move
and delete it. Each test checks the intended rule changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-11-08 12:51:27 +01:00
1 parent 019033e095
commit 684f051f0d
1 file changed
+114 -3
+114 -3
View File
@@ -2,7 +2,7 @@
//! inside the dialog, and only the visible rules are laid out, instead of one row per rule
//! making the window as tall as the whole list and the application window go blank.
use egui::vec2;
use egui::{Event, Modifiers, MouseWheelSource, MouseWheelUnit, PointerButton, Pos2, Rect, TouchPhase, Vec2, pos2, vec2};
use egui_kittest::kittest::Queryable;
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
@@ -14,10 +14,15 @@ const RULES: usize = 3000;
const SCREEN_H: f32 = 800.0;
fn harness_with_rules(n: usize) -> Harness {
let ranges: Vec<String> = (0..n).map(|i| format!("A{}", i + 1)).collect();
harness_with_ranges(&ranges)
}
/// One rule per entry of `ranges`, added in order (so the last one is listed first).
fn harness_with_ranges(ranges: &[String]) -> Harness {
let mut session = Session::new();
session.new_workbook();
for i in 0..n {
let range = format!("A{}", i + 1);
for range in ranges {
session
.execute(
"home.conditionalFormat",
@@ -38,6 +43,67 @@ fn harness_with_rules(n: usize) -> Harness {
h
}
fn dialog(h: &Harness) -> Rect {
h.get_by_label("Conditional Formatting Rules Manager").rect()
}
/// The rows of the list shown inside the dialog: (rule number, row rectangle).
fn visible_rows(h: &Harness) -> Vec<(usize, Rect)> {
let window = dialog(h);
let mut rows: Vec<(usize, Rect)> = h
.query_all_by_label_contains("applies to")
.filter(|n| window.contains(n.rect().left_center()))
.filter_map(|n| Some((n.value()?.split('.').next()?.parse().ok()?, n.rect())))
.collect();
rows.sort_by(|a, b| a.1.top().total_cmp(&b.1.top()));
rows
}
/// The row of rule `n`, wherever it is laid out (its label may reach past the dialog's edges).
fn row_of(h: &Harness, n: usize) -> Rect {
let prefix = format!("{n}. ");
h.query_all_by_label_contains("applies to")
.find(|r| r.value().is_some_and(|l| l.starts_with(&prefix)))
.expect("the rule's row is laid out")
.rect()
}
/// The `label` button on the row at `row`, if it is inside the dialog.
fn row_button(h: &Harness, row: Rect, label: &str) -> Option<Rect> {
let window = dialog(h);
h.query_all_by_label(label).map(|n| n.rect()).find(|r| window.contains_rect(*r) && (r.center().y - row.center().y).abs() < row.height() / 2.0)
}
fn wheel(h: &mut Harness, at: Pos2, delta: Vec2) {
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(2);
h.input_mut().events.push(Event::MouseWheel {
unit: MouseWheelUnit::Point,
delta,
phase: TouchPhase::Move,
source: MouseWheelSource::Unknown,
modifiers: Modifiers::NONE,
});
h.run_steps(30);
}
fn click(h: &mut Harness, r: Rect) {
let pos = r.center();
for pressed in [true, false] {
h.input_mut()
.events
.extend([Event::PointerMoved(pos), Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
h.step();
}
h.run_steps(4);
}
/// The ranges of every rule on the sheet, in list order.
fn rule_ranges(h: &Harness) -> Vec<String> {
let sh = h.state().session.active().unwrap().wb.active().unwrap();
sh.cond_formats.iter().map(|cf| cf.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>().join(",")).collect()
}
#[test]
fn many_rules_scroll_inside_a_dialog_that_fits_the_window() {
let mut h = harness_with_rules(RULES);
@@ -66,3 +132,48 @@ fn an_empty_sheet_says_so() {
let h = harness_with_rules(0);
h.get_by_label("There are no conditional formatting rules on this sheet.");
}
#[test]
fn a_rule_scrolled_into_view_is_moved_and_deleted() {
let mut h = harness_with_rules(RULES);
let before = visible_rows(&h);
let last_shown = before.iter().map(|r| r.0).max().expect("the first rules are listed");
// Scroll the list down, then act on a row in the middle of what is shown now.
let at = pos2(dialog(&h).left() + 40.0, before[before.len() / 2].1.center().y);
wheel(&mut h, at, vec2(0.0, -2000.0));
let rows = visible_rows(&h);
let (n, row) = rows[rows.len() / 2];
assert!(n > last_shown, "rule {n} was not on screen before scrolling (rules 1 to {last_shown} were)");
// Rule n is the n-th listed; new rules go first, so it applies to A{RULES + 1 - n}.
let target = format!("A{}", RULES + 1 - n);
assert_eq!(rule_ranges(&h)[n - 1], target);
let down = row_button(&h, row, "▼").expect("the row's move-down button is in the dialog");
click(&mut h, down);
let ranges = rule_ranges(&h);
assert_eq!(ranges[n], target, "rule {n} moved down one place");
assert_eq!(ranges[n - 1], format!("A{}", RULES - n), "the rule below it moved up");
// The moved rule is now rule n + 1, on the next row.
let delete = row_button(&h, row_of(&h, n + 1), "Delete").expect("the row's Delete button is in the dialog");
click(&mut h, delete);
let ranges = rule_ranges(&h);
assert_eq!(ranges.len(), RULES - 1);
assert!(!ranges.contains(&target), "the rule scrolled to was deleted");
}
#[test]
fn a_rule_with_a_long_label_is_reached_by_scrolling_sideways() {
let long: Vec<String> = (0..200).map(|i| format!("C{}", 2 * i + 1)).collect();
let long = long.join(",");
let mut h = harness_with_ranges(&["A1".into(), long.clone(), "B1".into()]);
assert_eq!(rule_ranges(&h), ["B1", long.as_str(), "A1"]);
let row = row_of(&h, 2);
assert!(row_button(&h, row, "▼").is_none(), "the long rule's buttons start out of view");
let window = dialog(&h);
wheel(&mut h, pos2(window.left() + 40.0, row.center().y), vec2(-20000.0, 0.0));
let down = row_button(&h, row_of(&h, 2), "▼").expect("scrolling sideways brings the move-down button into the dialog");
click(&mut h, down);
assert_eq!(rule_ranges(&h), ["B1", "A1", long.as_str()], "the long rule moved down");
let delete = row_button(&h, row_of(&h, 3), "Delete").expect("the long rule's Delete button is in the dialog on its new row");
click(&mut h, delete);
assert_eq!(rule_ranges(&h), ["B1", "A1"], "the long rule was deleted");
}